Conversation
- Implemented Download Orchestrator for high-level model download lifecycle management, consolidating logic from various SDKs. - Introduced File Manager for centralized file management operations, including directory creation, size calculation, and cache management. - Updated GeneratedPluginRegistrant for new plugins: CameraAndroidCameraxPlugin and ImagePickerPlugin. - Cleaned up and organized existing code for better maintainability.
upgrading cpp
…nanywhere-sdks into smonga/cpp_optm
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughMoves archive extraction and file-management into the native C++ core (libarchive), adds a download orchestrator and file-manager API, removes language-level archive libs, introduces JNI/FFI/Swift/Kotlin/Dart bridges, many new C APIs, thread-safety and OOM hardening, and bumps C++ standard to C++20. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App as Client / SDK
participant DM as Download Manager
participant DO as Download Orchestrator
participant EX as Native Extractor (libarchive)
participant FM as File Manager (platform callbacks)
participant MR as Model Registry
App->>+DM: rac_download_orchestrate(model_id, url, ...)
DM->>+DO: register task, compute destination
DO->>FM: ensure dirs / check storage (callbacks)
DO->>DM: request HTTP download via platform adapter
DM-->>DO: download complete (archive path)
DO->>+EX: rac_extract_archive_native(archivePath, destDir, options)
EX->>FM: write extracted files via platform callbacks
EX-->>-DO: extraction result (stats)
DO->>MR: resolve final model path after extraction
DO-->>-App: completion callback (final path / error)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (13)
sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp (1)
166-173:⚠️ Potential issue | 🟠 MajorRoll back the tracker if
out_generation_idallocation fails.Both start paths insert into
active_generationsbeforemalloc. If that allocation fails, the API returnsRAC_ERROR_OUT_OF_MEMORYbut leaves a live tracker behind, which can skew analytics state and accumulate stale entries.🔧 Minimal fix
handle->active_generations[id] = tracker; // Allocate and copy the ID for the caller *out_generation_id = static_cast<char*>(malloc(id.size() + 1)); if (!*out_generation_id) { + handle->active_generations.erase(id); return RAC_ERROR_OUT_OF_MEMORY; } memcpy(*out_generation_id, id.c_str(), id.size() + 1);Also applies to: 205-212
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp` around lines 166 - 173, The code stores tracker into handle->active_generations[id] before allocating *out_generation_id; if malloc fails you must roll back that insertion to avoid stale trackers: after the failed allocation return path, remove the entry (e.g. handle->active_generations.erase(id)) and release/cleanup tracker if it's heap-allocated, then return RAC_ERROR_OUT_OF_MEMORY; apply the same rollback/frees to the second occurrence around the 205–212 block as well.sdk/runanywhere-commons/src/infrastructure/storage/storage_analyzer.cpp (1)
114-127:⚠️ Potential issue | 🟠 MajorCompact failed entries before publishing
model_count.Line 126 skips the record, but
out_info->model_countstill reports the original registry count. Callers will iterate over a zeroed placeholder entry, so the OOM turns into inconsistent output rather than a true skip. Either compact successful metrics into a separate write index and publish that count, or fail the whole call.💡 One way to keep the result consistent
- out_info->model_count = model_count; + size_t valid_model_count = 0; out_info->total_models_size = 0; // Calculate metrics for each model for (size_t i = 0; i < model_count; i++) { const rac_model_info_t* model = models[i]; - rac_model_storage_metrics_t* metrics = &out_info->models[i]; + rac_model_storage_metrics_t* metrics = &out_info->models[valid_model_count]; // Copy model info metrics->model_id = model->id ? strdup(model->id) : nullptr; metrics->model_name = model->name ? strdup(model->name) : nullptr; if ((model->id && !metrics->model_id) || (model->name && !metrics->model_name)) { free(const_cast<char*>(metrics->model_id)); free(const_cast<char*>(metrics->model_name)); memset(metrics, 0, sizeof(rac_model_storage_metrics_t)); continue; } ... out_info->total_models_size += metrics->size_on_disk; + valid_model_count++; } + + out_info->model_count = valid_model_count;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/storage/storage_analyzer.cpp` around lines 114 - 127, The loop over model_count writes metrics into out_info->models but skips failed entries without updating out_info->model_count, producing inconsistent results; change the loop to use a separate write index (e.g., size_t write_i = 0) and on success move/assign metrics into out_info->models[write_i++] (or memcpy/strdup into that slot), and after the loop set out_info->model_count = write_i (or return an error on OOM instead of leaving zeroed placeholders); update references to metrics/model_id/model_name in the loop to write into the write index so only compacted, valid entries are published.sdk/runanywhere-commons/src/server/http_server.cpp (1)
116-137:⚠️ Potential issue | 🟠 MajorImmediate bind failures still sleep through the full startup timeout.
serverThread()only flipsrunning_after a successful bind. Whenbind_to_port()fails right away, this loop still sleeps for the full 100 × 100 ms before returningRAC_ERROR_SERVER_BIND_FAILED, so obvious startup failures look hung. Have the worker publish a startup result/state (or signal a condition variable) sostart()can return as soon as bind fails.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/server/http_server.cpp` around lines 116 - 137, The start() loop currently sleeps for SERVER_START_POLL_ITERATIONS × SERVER_START_POLL_MS even when bind_to_port() in serverThread() fails immediately; modify startup coordination so serverThread() signals a startup result (e.g., an atomic startup_result_ enum or a condition variable startup_cv_) right after bind_to_port() fails or succeeds, and change start() to wait on that condition variable (or poll startup_result_) with a timeout instead of blind sleeping. Specifically: add a startup_result_ (or reuse running_) that serverThread() sets to a distinct error state on bind failure, notify startup_cv_ after setting it, and in start() replace the for/sleep loop with startup_cv_.wait_for(...) (or a short-loop checking startup_result_) so start() can return RAC_ERROR_SERVER_BIND_FAILED immediately; keep cleanup steps (shouldStop_, join serverThread_, unloadModel(), server_.reset()) the same when notified of failure.sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp (2)
512-525:⚠️ Potential issue | 🟠 MajorClear
is_configuredbefore teardown starts.With
rac_voice_agent_is_ready()now doing a lock-free read, the agent still reports ready until Line 524, even though cleanup has already started. Move the store to the top ofrac_voice_agent_cleanup()so readiness flips before any component state is torn down.Suggested change
std::lock_guard<std::mutex> lock(handle->mutex); RAC_LOG_INFO("VoiceAgent", "Cleaning up Voice Agent"); +handle->is_configured.store(false, std::memory_order_release); // Cleanup all components (mirrors Swift's cleanup) rac_llm_component_cleanup(handle->llm_handle); rac_stt_component_cleanup(handle->stt_handle); rac_tts_component_cleanup(handle->tts_handle); // VAD uses stop + reset instead of cleanup rac_vad_component_stop(handle->vad_handle); rac_vad_component_reset(handle->vad_handle); - handle->is_configured.store(false, std::memory_order_release); - return RAC_SUCCESS;As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".
Also applies to: 529-535
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around lines 512 - 525, The readiness flag is flipped too late: in rac_voice_agent_cleanup() move the atomic write that clears handle->is_configured (handle->is_configured.store(false, std::memory_order_release)) to the very start of rac_voice_agent_cleanup(), before acquiring the mutex and before any component teardown calls (rac_llm_component_cleanup, rac_stt_component_cleanup, rac_tts_component_cleanup, rac_vad_component_stop/reset) so that rac_voice_agent_is_ready() (which does a lock-free read) immediately reports not-ready; ensure the same change is applied to the other cleanup block referenced around lines 529-535.
668-799:⚠️ Potential issue | 🟠 MajorStage the stream events before invoking the callback.
At Line 669 the mutex stays held across every error/success callback. That can deadlock if the callback re-enters any
rac_voice_agent_*API, and it also leaveswav_copyto be allocated only afterRAC_VOICE_AGENT_EVENT_AUDIO_SYNTHESIZEDhas already been emitted. If that late allocation fails,RAC_VOICE_AGENT_EVENT_PROCESSEDsilently loses audio while the function still returns success. Build/copy both event payloads first, release the mutex, then invoke the callbacks.As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around lines 668 - 799, The function holds handle->mutex while invoking callback and performing late allocations (wav_copy), which can deadlock or lose audio on allocation failure; change rac_voice_agent_process (the pipeline block using rac_stt_component_transcribe, rac_llm_component_generate, rac_tts_component_synthesize, rac_audio_float32_to_wav) to fully build/copy all event payloads (transcription_event, response_event, audio_event with wav_copy, processed_event) while still under the lock, then release handle->mutex before calling callback for any event; ensure wav_copy is allocated/copied before releasing the lock and ensure all error callbacks are also dispatched after the lock is released so callbacks cannot re-enter rac_voice_agent_* APIs while the mutex is held.sdk/runanywhere-commons/src/features/tts/tts_analytics.cpp (1)
147-153:⚠️ Potential issue | 🟡 MinorAvoid leaving a phantom synthesis tracked on OOM.
The tracker is inserted before
out_synthesis_idis allocated. Ifmallocfails here,rac_tts_analytics_start_synthesisreturnsRAC_ERROR_OUT_OF_MEMORYbut leaves an unreachable entry inactive_syntheses, which permanently skews this handle’s state.💡 Suggested fix
- handle->active_syntheses[id] = tracker; - *out_synthesis_id = static_cast<char*>(malloc(id.size() + 1)); if (!*out_synthesis_id) { return RAC_ERROR_OUT_OF_MEMORY; } memcpy(*out_synthesis_id, id.c_str(), id.size() + 1); + handle->active_syntheses[id] = tracker;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/tts/tts_analytics.cpp` around lines 147 - 153, In rac_tts_analytics_start_synthesis the tracker is stored into handle->active_syntheses[id] before allocating *out_synthesis_id, so on malloc failure the phantom tracker remains; fix by either allocating and assigning *out_synthesis_id (using id.size()+1) before inserting tracker, or if you keep the current order then on malloc failure free any resources and erase handle->active_syntheses[id] (and/or delete tracker) before returning RAC_ERROR_OUT_OF_MEMORY; ensure you reference handle->active_syntheses, out_synthesis_id, id, tracker and rac_tts_analytics_start_synthesis when making the change.sdk/runanywhere-commons/src/features/tts/tts_component.cpp (1)
12-17:⚠️ Potential issue | 🟡 MinorInclude
<new>explicitly in this translation unit.Line 115 uses
new (std::nothrow), but this file does not explicitly include<new>. Relying on transitive includes is brittle and can become a compile break as headers change.Minimal fix
`#include` <cstring> `#include` <mutex> +#include <new> `#include` <random> `#include` <string>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/tts/tts_component.cpp` around lines 12 - 17, The file tts_component.cpp uses placement new with std::nothrow (new (std::nothrow) at or around line 115) but does not explicitly include the <new> header; add an explicit `#include` <new> to the top-of-file includes so the use of std::nothrow is defined and the translation unit no longer relies on transitive includes (update the include list near the other standard headers in tts_component.cpp).sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)
477-508:⚠️ Potential issue | 🟠 Major
get_state()/get_metrics()still don't represent the component lifecycle.Both APIs are derived from a single
is_initializedbit, so callers never observestart/stop/resettransitions and the metrics stay stuck at a synthetic “1 successful load”. This needs a dedicated lifecycle state/counter model, not a boolean approximation. As per coding guidelines "Component lifecycle implementations must support all rac_lifecycle_state_t transitions and track state changes".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 477 - 508, The current get_state (rac_vad_component_get_state) and get_metrics (rac_vad_component_get_metrics) use only component->is_initialized, so they never expose start/stop/reset transitions or proper counters; add a real lifecycle model to rac_vad_component (e.g., a rac_lifecycle_state_t state field and integer counters like total_loads, successful_loads, start_count, stop_count, reset_count), update all lifecycle transition points (init/load/start/stop/reset) to set component->state and increment the appropriate counters under component->mtx, then change rac_vad_component_get_state to return component->state and rac_vad_component_get_metrics to populate out_metrics from those counters (instead of the boolean-based logic) while keeping the same error checks and mutex usage.sdk/runanywhere-commons/src/features/vad/energy_vad.cpp (1)
279-320:⚠️ Potential issue | 🟠 Major
RAC_ERROR_OUT_OF_MEMORYis still bypassed after thenothrowallocation.
new (std::nothrow) rac_energy_vad()only protects the object itself. The laterrecent_energy_values.resize(...)uses the default allocator, which throwsstd::bad_allocon failure. This C API may still throw/terminate instead of returningRAC_ERROR_OUT_OF_MEMORY.🔧 Possible fix
rac_energy_vad* vad = new (std::nothrow) rac_energy_vad(); if (!vad) { return RAC_ERROR_OUT_OF_MEMORY; } + + try { + vad->recent_energy_values.resize(vad->max_recent_values, 0.0f); + } catch (const std::bad_alloc&) { + delete vad; + return RAC_ERROR_OUT_OF_MEMORY; + } // Initialize from config (mirrors Swift init) vad->sample_rate = cfg->sample_rate; @@ - vad->recent_energy_values.resize(vad->max_recent_values, 0.0f);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/energy_vad.cpp` around lines 279 - 320, The allocation with new (std::nothrow) rac_energy_vad() protects only the object but recent_energy_values.resize(...) can throw std::bad_alloc; wrap the resize call in a try/catch that catches std::bad_alloc (or std::exception), delete the newly created vad on failure and return RAC_ERROR_OUT_OF_MEMORY; reference the rac_energy_vad allocation, recent_energy_values.resize, and ensure vad is cleaned up (delete vad) before returning the error so the C API never lets an exception escape.sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart (1)
25-35:⚠️ Potential issue | 🟠 MajorDon’t treat a failed storage check as “enough space.”
Line 31 now depends on the native file-manager bridge, but the catch path still returns
true. If that bridge is not registered yet or the FFI call fails, downloads will proceed without any storage guard.🛡️ Minimal safe fallback
static Future<bool> checkStorageAvailable({ required int modelSize, double safetyMargin = 0.1, }) async { try { final requiredWithMargin = (modelSize * (1 + safetyMargin)).toInt(); return DartBridgeFileManager.checkStorage(requiredWithMargin); } catch (_) { - // Default to available if check fails - return true; + return false; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart` around lines 25 - 35, checkStorageAvailable currently treats any exception from DartBridgeFileManager.checkStorage as "enough space"; change the catch block in checkStorageAvailable to use a conservative fallback (return false) instead of true, and log the caught error (include error details and context mentioning DartBridgeFileManager.checkStorage and requiredWithMargin) so failures of the native bridge don't allow unsafe downloads; ensure the method still returns a bool and keeps the requiredWithMargin calculation intact.sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp (1)
1233-1250:⚠️ Potential issue | 🟠 MajorKeep registry removal and file deletion in one success path.
Line 1243 removes the registry entry, while Line 1247 deletes files independently of that result. If registry removal fails, this can still delete the files and leave the registry pointing at missing content; if file deletion fails, the method still reports success based only on the registry result.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp` around lines 1233 - 1250, The current deleteModel flow removes the registry entry then always attempts file deletion and returns only the registry result; change it so registry removal and file deletion form a single success path: in HybridRunAnywhereCore::deleteModel, after obtaining modelInfo/framework, call ModelRegistryBridge::shared().removeModel(modelId) and if that fails do not call FileManagerBridge::shared().deleteModel and return failure; if removeModel succeeds, call FileManagerBridge::shared().deleteModel and capture its success; if file deletion fails, attempt to restore the registry entry using the previously fetched modelInfo via ModelRegistryBridge::shared().addModel(modelInfo) (or the appropriate restore method) and return failure; only return true when both removeModel and file deletion succeed.sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService+Execution.swift (1)
77-103:⚠️ Potential issue | 🟠 MajorAdd cleanup calls for stored download requests in both success and failure paths.
storeDownloadRequest(downloadRequest, forKey: taskId)at line 77 adds an entry toactiveDownloadRequests, but neither the success case (line 83/87) nor the failure case (line 97) removes it. TheremoveDownloadRequestmethod exists but is never called, causing stored requests to accumulate indefinitely and creating a resource leak. Addself.removeDownloadRequest(forKey: taskId)before eachcontinuation.resume()call.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService`+Execution.swift around lines 77 - 103, The stored download request added via storeDownloadRequest(downloadRequest, forKey: taskId) is never removed; call self.removeDownloadRequest(forKey: taskId) inside the downloadRequest.response closure immediately before every continuation.resume(returning:) and continuation.resume(throwing:) so both the success path (where downloadedURL is non-nil) and all failure branches (invalidResponse and .failure mapped via mapAlamofireError) remove the entry; update the closure around withCheckedThrowingContinuation to invoke removeDownloadRequest(forKey: taskId) just prior to resuming the continuation in each case.sdk/runanywhere-react-native/packages/core/src/services/FileSystem.ts (1)
285-298:⚠️ Potential issue | 🟠 Major
modelExists()no longer verifies the expected artifact.After this change, any non-empty model folder returns
true. A failed extraction, straymmproj, or leftover temp file will now look like a valid install and can block redownload/recovery.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-react-native/packages/core/src/services/FileSystem.ts` around lines 285 - 298, The modelExists function currently returns true for any non-empty folder; update modelExists to verify presence of framework-specific artifact(s) instead of just folder non-emptiness: use the fw parameter to switch on expected filenames/extensions (e.g., for 'ONNX' require a .onnx file, for TensorFlow/TFLite require the specific .tflite/.pb or bundle filenames your app expects, etc.), inspect the RNFS.readDir(folder) results for those expected entries (by name or extension) and only return true if the required artifact(s) are found; keep the early exists check but replace the generic files.length check with the targeted artifact presence check inside modelExists.
🟡 Minor comments (11)
sdk/runanywhere-commons/src/utils/rac_image_utils.cpp-316-318 (1)
316-318:⚠️ Potential issue | 🟡 MinorGood validation, but inconsistent error code.
The channel validation correctly prevents out-of-bounds access to the mean/std arrays (size 3). However, other validation checks in this file use
RAC_ERROR_INVALID_ARGUMENT(e.g., lines 251, 396, 426), while this usesRAC_ERROR_INVALID_PARAMETER.Proposed fix for consistency
if (image->channels < 1 || image->channels > 3) { - return RAC_ERROR_INVALID_PARAMETER; + return RAC_ERROR_INVALID_ARGUMENT; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/utils/rac_image_utils.cpp` around lines 316 - 318, The channel validation returns RAC_ERROR_INVALID_PARAMETER but other validations in this file use RAC_ERROR_INVALID_ARGUMENT; update the check that inspects image->channels (the if (image->channels < 1 || image->channels > 3) branch) to return RAC_ERROR_INVALID_ARGUMENT instead of RAC_ERROR_INVALID_PARAMETER so error codes are consistent across the file.sdk/runanywhere-commons/src/infrastructure/network/api_types.cpp-533-539 (1)
533-539:⚠️ Potential issue | 🟡 MinorAdd explicit include for
SIZE_MAXdefinition.The overflow check correctly guards against
size_twraparound: the logicevent_count > (SIZE_MAX - kBaseEstimate) / kPerEventEstimateensures the subsequent multiplication and addition won't overflow.However,
SIZE_MAXis not explicitly included. It's defined in<climits>or<cstdint>, neither of which appears in this file's includes. While some implementations transitively include it through<cstdlib>, this is not guaranteed by the C++ standard—particularly important given that this project explicitly usesgcc/g++and notes that clang lacks proper C++ stdlib header support on the target platform. Per the Google C++ Style Guide followed by this project, add an explicit include:`#include` <climits>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/network/api_types.cpp` around lines 533 - 539, Add an explicit include for the header that defines SIZE_MAX by inserting `#include` <climits> among the other includes at the top of api_types.cpp so the overflow check that uses SIZE_MAX (around the calculation with kPerEventEstimate, kBaseEstimate, and batch->event_count) is well-defined across toolchains; ensure the new include is placed with the existing system headers.sdk/runanywhere-commons/src/features/llm/llm_component.cpp-706-712 (1)
706-712:⚠️ Potential issue | 🟡 MinorEmit a terminal failure event on this OOM return.
This branch returns before
rac_lifecycle_track_error()andRAC_EVENT_LLM_GENERATION_FAILED, so a started stream can disappear from telemetry if the finalstrdup()fails.Suggested patch
final_result.text = strdup(ctx.full_text.c_str()); if (!final_result.text) { log_error("LLM.Component", "Failed to allocate result text"); + rac_lifecycle_track_error(component->lifecycle, RAC_ERROR_OUT_OF_MEMORY, + "generateStream"); + + rac_analytics_event_data_t event = {}; + event.type = RAC_EVENT_LLM_GENERATION_FAILED; + event.data.llm_generation = RAC_ANALYTICS_LLM_GENERATION_DEFAULT; + event.data.llm_generation.generation_id = generation_id.c_str(); + event.data.llm_generation.model_id = model_id; + event.data.llm_generation.model_name = model_name; + event.data.llm_generation.error_code = RAC_ERROR_OUT_OF_MEMORY; + event.data.llm_generation.error_message = "Failed to allocate result text"; + rac_analytics_event_emit(RAC_EVENT_LLM_GENERATION_FAILED, &event); + if (error_callback) { error_callback(RAC_ERROR_OUT_OF_MEMORY, "Failed to allocate result text", user_data); } return RAC_ERROR_OUT_OF_MEMORY; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/llm/llm_component.cpp` around lines 706 - 712, The OOM branch that returns when final_result.text is null should also emit the terminal failure to telemetry: before returning from the allocation check in the LLM component, call rac_lifecycle_track_error(...) with the appropriate context and error code and emit RAC_EVENT_LLM_GENERATION_FAILED (via the existing event tracking function used elsewhere) so the started stream is marked failed; keep the existing log_error and error_callback but add the rac_lifecycle_track_error(...) and the event emit (including any message/user_data) immediately prior to returning RAC_ERROR_OUT_OF_MEMORY.sdk/runanywhere-commons/src/features/stt/stt_component.cpp-342-345 (1)
342-345:⚠️ Potential issue | 🟡 MinorUse the resolved request
sample_ratein telemetry.
local_optionsis the effective per-call config, butsample_rateis copied fromcomponent->config.sample_rate. If callers overrideoptions->sample_rate, the started/completed events will report the default instead of the value actually used for transcription.🔧 Minimal fix
- sample_rate = component->config.sample_rate; - // Copy effective options to local so we can release the lock local_options = options ? *options : component->default_options; + sample_rate = local_options.sample_rate;Also applies to: 382-385, 441-443
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/stt/stt_component.cpp` around lines 342 - 345, Telemetry uses component->config.sample_rate instead of the per-call resolved value; after copying the effective per-call config into local_options (from options ? *options : component->default_options), assign sample_rate = local_options.sample_rate and use that for the started/completed events so overridden options->sample_rate is reported; apply the same change in the other occurrences where sample_rate is taken from component->config (the other blocks around the start/completed event emission).sdk/runanywhere-commons/src/features/stt/stt_component.cpp-16-16 (1)
16-16:⚠️ Potential issue | 🟡 MinorAdd the missing header for
snprintf.Line 70 uses
snprintf, but<cstdio>is not included. The code currently relies on transitive declarations, which is not portable. Add<cstdio>to the includes and preferably usestd::snprintffor C++17 compliance.Suggested fix
`#include` <chrono> +#include <cstdio> `#include` <cstdlib> `#include` <cstring> `#include` <mutex> `#include` <random>Then at line 70:
- snprintf(buffer, sizeof(buffer), "trans_%08x%08x", dis(gen), dis(gen)); + std::snprintf(buffer, sizeof(buffer), "trans_%08x%08x", dis(gen), dis(gen));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/stt/stt_component.cpp` at line 16, Add the missing C stdio header and prefer the C++-qualified function: include <cstdio> alongside the existing includes (the file currently only shows `#include` <random>) and replace or call the function as std::snprintf where snprintf is used (the call around line 70) so the code no longer relies on transitive declarations and is C++17-compliant.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt-211-212 (1)
211-212:⚠️ Potential issue | 🟡 MinorAdd
CppBridgeFileManager.unregister()and call it during shutdown for consistency.
CppBridgeFileManager.register()registers JNI file I/O callbacks, but there is no correspondingunregister()method or cleanup inshutdown(). Every other bridge extension follows a symmetric register/unregister lifecycle. Add anunregister()method toCppBridgeFileManagerand call it inCppBridge.shutdown()to ensure proper cleanup on shutdown cycles and match the established pattern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt` around lines 211 - 212, Add a symmetric cleanup path for the file manager: implement CppBridgeFileManager.unregister() to undo whatever CppBridgeFileManager.register() sets up (remove JNI callbacks/resources) and invoke CppBridgeFileManager.unregister() from CppBridge.shutdown() (alongside other bridge unregister calls) so the CppBridge.shutdown() lifecycle mirrors registration and performs proper cleanup.sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeFileManager.kt-115-123 (1)
115-123:⚠️ Potential issue | 🟡 MinorOnly treat an existing directory as success.
dir.exists()is also true for a regular file, so this reports success even when the target path is unusable as a directory. CheckisDirectorybefore returning success.Suggested fix
- if (success || dir.exists()) 0 else -180 // RAC_ERROR_DIRECTORY_CREATION_FAILED + if (success || dir.isDirectory) 0 else -180 // RAC_ERROR_DIRECTORY_CREATION_FAILED🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeFileManager.kt` around lines 115 - 123, In createDirectory(path: String, recursive: Boolean) update the success condition so an existing regular file isn't treated as a successful directory creation: after attempting dir.mkdirs()/dir.mkdir() only return success (0) if the operation succeeded or if dir.isDirectory() is true; otherwise return the error code -180. Ensure the catch block still returns -180 and reference the createDirectory function and the local variable dir when making this change.sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/ExtractionService.swift-149-153 (1)
149-153:⚠️ Potential issue | 🟡 MinorDon't hard-code
fileCountto zero.Every successful extraction now reports
fileCount: 0, which makesExtractionResultinternally inconsistent and drops extraction stats for any downstream UI or analytics.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/ExtractionService.swift` around lines 149 - 153, The return currently hard-codes fileCount: 0 in the ExtractionResult creating an incorrect metric; change it to return the actual number of files produced by the extraction (e.g. use the count of the extracted files collection or compute files under modelPath) so ExtractionResult(modelPath: modelPath, extractedSize: extractedSize, fileCount: <actualCount>, durationSeconds: duration) reflects the real value—look for the extraction routine's list/array (e.g. extractedFiles, filesExtracted, or use FileManager.contentsOfDirectory(atPath:) on modelPath) and substitute that count for fileCount.sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp-171-174 (1)
171-174:⚠️ Potential issue | 🟡 MinorThis depth guard skips the last level.
With
depth >= max_depth, a search starting atdepth = 0never inspects levelmax_depthitself. The currentmax_depth = 2call only visits depths 0 and 1.Suggested fix
- if (depth >= max_depth) return false; + if (depth > max_depth) return false;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp` around lines 171 - 174, The depth check in find_single_model_file incorrectly stops when depth == max_depth so the final level is never searched; change the guard from "if (depth >= max_depth) return false;" to "if (depth > max_depth) return false;" (and ensure any recursive calls pass depth+1 so the function can execute at depth == max_depth) so the function will inspect the max_depth level as intended; reference: function find_single_model_file, parameters depth and max_depth, and output buffer out_path.sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp-4306-4314 (1)
4306-4314:⚠️ Potential issue | 🟡 MinorMissing exception check after
NewStringUTF.If
NewStringUTFfails (OOM), it returnsnullptrand throws a pending exception. Calling JNI methods with a pending exception causes undefined behavior.🛡️ Proposed fix
static rac_result_t jni_fc_create_directory(const char* path, int recursive, void* user_data) { JNIEnv* env = getJNIEnv(); if (env == nullptr || g_file_callbacks_obj == nullptr) return RAC_ERROR_NOT_INITIALIZED; jstring jPath = env->NewStringUTF(path); + if (jPath == nullptr) { + if (env->ExceptionCheck()) env->ExceptionClear(); + return RAC_ERROR_OUT_OF_MEMORY; + } jint result = env->CallIntMethod(g_file_callbacks_obj, g_fc_create_directory, jPath, static_cast<jboolean>(recursive != 0)); env->DeleteLocalRef(jPath); return static_cast<rac_result_t>(result); }The same pattern should be applied to
jni_fc_delete_path,jni_fc_list_directory,jni_fc_path_exists, andjni_fc_get_file_size.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 4306 - 4314, The NewStringUTF call in jni_fc_create_directory can return nullptr and leave a pending exception (OOM); update jni_fc_create_directory to check if jPath is nullptr or env->ExceptionCheck() is true after NewStringUTF, clean up any local refs if needed, clear or leave the exception handled appropriately and return RAC_ERROR_NOT_INITIALIZED (or an appropriate rac_result_t) instead of calling CallIntMethod; apply the same null/exception-check pattern to jni_fc_delete_path, jni_fc_list_directory, jni_fc_path_exists, and jni_fc_get_file_size so none call JNI methods when NewStringUTF failed.sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp-4427-4454 (1)
4427-4454:⚠️ Potential issue | 🟡 MinorMissing verification that all method IDs were found.
If any
GetMethodIDcall fails (method doesn't exist on the Kotlin side), it returnsnullptrand sets a pending exception. The code should verify all method IDs before returning success.🛡️ Proposed fix to verify method IDs
g_fc_get_available_space = env->GetMethodID(cls, "getAvailableSpace", "()J"); g_fc_get_total_space = env->GetMethodID(cls, "getTotalSpace", "()J"); env->DeleteLocalRef(cls); + // Verify all required methods were found + if (!g_fc_create_directory || !g_fc_delete_path || !g_fc_list_directory || + !g_fc_path_exists || !g_fc_is_directory || !g_fc_get_file_size || + !g_fc_get_available_space || !g_fc_get_total_space) { + LOGe("File manager callbacks: missing required methods"); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + env->DeleteGlobalRef(g_file_callbacks_obj); + g_file_callbacks_obj = nullptr; + return RAC_ERROR_INVALID_ARGUMENT; + } + LOGi("File manager callbacks registered"); return RAC_SUCCESS;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 4427 - 4454, Verify that every cached jmethodID (g_fc_create_directory, g_fc_delete_path, g_fc_list_directory, g_fc_path_exists, g_fc_is_directory, g_fc_get_file_size, g_fc_get_available_space, g_fc_get_total_space) is non-null after the GetMethodID calls in nativeFileManagerRegisterCallbacks; if any are null, clean up by deleting the global ref g_file_callbacks_obj (and set it to nullptr), delete the local class ref, check/clear the pending JNI exception via env->ExceptionCheck()/env->ExceptionClear(), and return an appropriate error code (e.g., RAC_ERROR_METHOD_NOT_FOUND) instead of RAC_SUCCESS. Ensure normal flow returns RAC_SUCCESS only when all method IDs are valid.
🧹 Nitpick comments (9)
sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Services/SimplifiedFileManager.swift (1)
121-124: Potential main-thread blocking and code duplication.
@mainactor with I/O: This method calls the C++ bridge for file system checks while constrained to the main thread, which could cause UI hitches if the operation takes time.
Duplication: This method is identical to
modelFolderExists(modelId:framework:)at line 61. Consider having one call the other or extracting to a single implementation.♻️ Suggested refactor
`@MainActor` public func isModelDownloaded(modelId: String, framework: InferenceFramework) -> Bool { - return CppBridge.FileManager.modelFolderHasContents(modelId: modelId, framework: framework) + return modelFolderExists(modelId: modelId, framework: framework) }Also consider whether
@MainActoris necessary here, or if this could be madenonisolatedor async to avoid blocking the main thread.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Services/SimplifiedFileManager.swift` around lines 121 - 124, The isModelDownloaded(modelId:framework:) method duplicates modelFolderExists(modelId:framework:) and is marked `@MainActor` while calling a potentially blocking C++ file-system check; change isModelDownloaded to reuse the existing implementation by delegating to modelFolderExists(modelId:framework:) (or extract a shared private helper that calls CppBridge.FileManager.modelFolderHasContents) and remove the `@MainActor` isolation so the call won’t block the main thread—alternatively make it async/nonisolated if the surrounding API expects concurrency; ensure any shared helper has appropriate thread-safety if necessary.sdk/runanywhere-commons/src/utils/rac_image_utils.cpp (1)
493-497: Reasonable defensive fix; consider logging a warning.Clamping invalid dimensions to 1x1 prevents division by zero in the aspect ratio calculation below. However, silently fixing invalid input may mask bugs in upstream code. A debug log would help diagnose issues without changing behavior.
Optional: Add debug logging for invalid dimensions
if (width <= 0 || height <= 0) { + RAC_LOG_DEBUG(LOG_CAT, "Invalid dimensions (%dx%d), clamping to 1x1", width, height); *out_width = 1; *out_height = 1; return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/utils/rac_image_utils.cpp` around lines 493 - 497, When detecting invalid input in the width/height check (the if (width <= 0 || height <= 0) block that sets *out_width and *out_height to 1), add a debug/warning log before clamping so upstream issues are visible; emit a concise message that includes the offending width and height values and the function/variable context (width, height, out_width, out_height) using the project’s existing logger API (e.g., LOG_WARN/LOG_DEBUG or the module’s logger) while keeping the current behavior of setting outputs to 1 and returning.sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_file_manager.h (1)
287-299: Don't publish an "internal helper" asRAC_API.Line 298 exports
rac_file_manager_clear_directoryfrom the public header while the section/doc call it internal. That hardens a private helper into supported API surface for every SDK consumer. Either move it to a private header or document it as a stable public primitive.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_file_manager.h` around lines 287 - 299, The declaration rac_file_manager_clear_directory is marked with RAC_API in the public header but is described as an internal helper; remove it from the public API by either (A) moving the declaration (and any related types like rac_file_callbacks_t usage) into a private/internal header or compilation unit and removing the RAC_API annotation from the public header, or (B) if it must remain public, update the header comment and documentation to declare rac_file_manager_clear_directory as a supported public primitive and keep RAC_API; choose one approach and apply consistently to avoid exporting an unintended private symbol.sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart (1)
1473-1484: Document memory ownership forrecommendationpointer.
RacStorageAvailabilityStruct.recommendationis aPointer<Utf8>that callers must handle correctly. Consider adding a doc comment clarifying whether this string is owned by the struct (and freed with it) or must be freed separately by the caller.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart` around lines 1473 - 1484, RacStorageAvailabilityStruct.recommendation is a raw Pointer<Utf8> with no documented ownership/lifetime — add a doc comment on the recommendation field (and near the RacStorageAvailabilityStruct declaration) that states whether the pointer is owned by the struct or by the caller, whether the string is null-terminated, how long the pointer is valid (e.g. only until the struct is freed or until an explicit free), and the exact method to release it if the caller must free it (e.g. call freeString(), free via malloc.free, or use a provided API function); reference RacStorageAvailabilityStruct.recommendation in the comment so callers know to follow the specified freeing convention.sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp (1)
301-302: Good security practice clearing sensitive data from stack.Zeroing the buffer after loading tokens helps prevent sensitive credential leakage via stack inspection. Note that aggressive compiler optimizations may elide this
memsetif the buffer is not subsequently used. For stronger guarantees, consider usingexplicit_bzero(POSIX) orSecureZeroMemory(Windows) if available.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp` around lines 301 - 302, The current memset(buffer, 0, sizeof(buffer)) can be optimized away by compilers; replace this with a platform-appropriate secure zero routine to ensure memory is wiped (e.g., call explicit_bzero(buffer, sizeof(buffer)) on POSIX, SecureZeroMemory(buffer, sizeof(buffer)) on Windows, or a fallback to a volatile memset wrapper if neither is available), keeping the same buffer identifier so the sensitive token storage is cleared reliably; update any `#includes` and add conditional compilation helpers (or a small secure_zero(void*, size_t) wrapper) and use that in place of the existing memset call.sdk/runanywhere-commons/tests/test_download_orchestrator.cpp (1)
42-50: Preferstd::filesystemover shelling out in these helpers.
system("rm -rf ...")/system("mkdir -p ...")makes the tests depend on/bin/shand still leaves quoting edge cases. C++17 already gives youremove_allandcreate_directoriesin-process.Possible cleanup
Add
#include <filesystem>near the other headers, then:static void remove_dir(const std::string& path) { - std::string cmd = "rm -rf \"" + path + "\""; - system(cmd.c_str()); + std::error_code ec; + std::filesystem::remove_all(path, ec); } static void mkdir_p(const std::string& path) { - std::string cmd = "mkdir -p \"" + path + "\""; - system(cmd.c_str()); + std::error_code ec; + std::filesystem::create_directories(path, ec); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/tests/test_download_orchestrator.cpp` around lines 42 - 50, Replace shelling out in the helpers by using std::filesystem: include <filesystem>, change remove_dir to call std::filesystem::remove_all(path) and change mkdir_p to call std::filesystem::create_directories(path); ensure you handle/propagate errors or ignore return values consistently (remove_all returns count, create_directories returns bool) and remove the system(...) calls in remove_dir and mkdir_p.sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h (1)
82-135: Document the callback/lifetime contract in the public header.This API documents parameters and error codes, but not whether
rac_extraction_progress_fnis synchronous, which thread invokes it, or how longuser_dataandout_resultmust remain valid. That lifecycle contract belongs in the header for SDK integrators. As per coding guidelines, "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h` around lines 82 - 135, The header is missing the lifecycle and threading contract for rac_extraction_progress_fn and the pointer parameters; update the comments for rac_extraction_progress_fn and rac_extract_archive_native to state explicitly whether the callback is invoked synchronously or on a separate thread, whether it may be re-entered or called concurrently, and the valid lifetimes for user_data and out_result. Specifically: document that rac_extraction_progress_fn is invoked by rac_extract_archive_native during the extraction operation (synchronously on the extraction thread/caller thread), that the callback will not be called after rac_extract_archive_native returns, and that the caller must keep user_data and the out_result storage valid for the entire duration of rac_extract_archive_native; also note thread-safety expectations (e.g., callback may be called serially, not concurrently) and mention behavior if async/streaming options change (point to rac_extraction_options_t if it can enable async behavior). Include these lifecycle sentences in the public header adjacent to the typedef for rac_extraction_progress_fn and the rac_extract_archive_native declaration so integrators know the contract.sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (1)
4294-4304: File callbacks globals lack thread-safety protection.The global variables
g_file_callbacks_objand cached method IDs are accessed without mutex protection. IfnativeFileManagerRegisterCallbacksis called while a file operation is in progress on another thread, this could cause a race condition.Consider adding a mutex (similar to
g_adapter_mutexused elsewhere in this file) if concurrent registration is possible.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 4294 - 4304, The globals for file callbacks (g_file_callbacks_obj, g_fc_create_directory, g_fc_delete_path, g_fc_list_directory, g_fc_path_exists, g_fc_is_directory, g_fc_get_file_size, g_fc_get_available_space, g_fc_get_total_space) need mutex protection to avoid races during registration and use; add a dedicated mutex (or reuse g_adapter_mutex) and lock it around writes in nativeFileManagerRegisterCallbacks and around reads/uses in any file operation functions that access these globals, ensure proper JNI local/global reference handling when updating g_file_callbacks_obj while holding the lock, and release/clear the global safely when unregistering.sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h (1)
38-115: Document thread-safety guarantees and callback lifecycle requirements.Per coding guidelines, public C API headers in
include/rac/must document lifecycle requirements. Therac_file_callbacks_tstruct should clarify:
- Whether callbacks must remain valid for the duration of any API call using them
- Thread-safety requirements (can the same callbacks struct be used from multiple threads?)
- Whether
user_dataownership is transferred or borrowedConsider adding a brief note, e.g.:
/** * `@note` Callbacks must remain valid for the duration of any rac_file_manager_* call. * `@note` Thread safety: Callers must synchronize concurrent access to shared user_data. */Based on learnings: "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.h` around lines 38 - 115, Add lifecycle and thread-safety notes to the rac_file_callbacks_t declaration: state that callbacks in rac_file_callbacks_t must remain valid for the duration of any API call that receives the struct (e.g., rac_file_manager_* functions), clarify that user_data is borrowed (ownership not transferred) unless otherwise documented, and require callers to synchronize concurrent access if the same rac_file_callbacks_t (or its user_data) is used from multiple threads; reference the struct name rac_file_callbacks_t and the user_data field in the note so readers can find the relevant vtable and understand callback lifetime and thread-safety constraints.
| // Acquire lock only for state reads; release before long-running generation | ||
| rac_handle_t service = nullptr; | ||
| rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service); | ||
| if (result != RAC_SUCCESS) { | ||
| RAC_LOG_ERROR("Diffusion.Component", "No model loaded - cannot generate"); | ||
| return result; | ||
| } | ||
| rac_diffusion_options_t effective_options; | ||
| { | ||
| std::lock_guard<std::mutex> lock(component->mtx); | ||
|
|
||
| // Reset cancellation flag (also atomic, but set under lock for consistency) | ||
| component->cancel_requested = false; | ||
|
|
||
| // Get service from lifecycle manager | ||
| rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service); | ||
| if (result != RAC_SUCCESS) { | ||
| RAC_LOG_ERROR("Diffusion.Component", "No model loaded - cannot generate"); | ||
| return result; | ||
| } | ||
|
|
||
| // Merge user options over component defaults | ||
| rac_diffusion_options_t effective_options = merge_diffusion_options( | ||
| component->default_options, options); | ||
| // Merge user options over component defaults | ||
| effective_options = merge_diffusion_options(component->default_options, options); | ||
| } | ||
| // Lock released — safe to do long-running generation |
There was a problem hiding this comment.
Keep the lifecycle service pinned for the full generation call.
After Line 408 and Line 506, load_model(), unload(), or cleanup() can acquire component->mtx and replace/reset the raw service handle while rac_diffusion_generate*() is still using it. The same change also makes concurrent generate*() calls share/reset one cancel_requested flag, so one request can cancel or revive another; either serialize generation again or add explicit service acquire/release semantics plus per-request cancellation state. As per coding guidelines, sdk/runanywhere-commons/src/**/*component*.{cpp,h}: Component lifecycle implementations must support all rac_lifecycle_state_t transitions and track state changes.
Also applies to: 418-419, 485-507, 524-527
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp`
around lines 390 - 409, The code currently releases component->mtx before
calling rac_diffusion_generate*(), allowing load_model()/unload()/cleanup() to
replace the raw service handle or reset component->cancel_requested while
generation is in progress; fix by keeping the lifecycle service pinned for the
full generation call and using per-request cancellation state: call
rac_lifecycle_require_service (or the lifecycle pin API) to obtain/increment a
reference to the service handle (the same call used earlier) and store that
local pinned service, clear only a per-request cancel flag (not
component->cancel_requested) or allocate a request-local cancel token, then
perform rac_diffusion_generate*(pinned_service, ...) and finally call
rac_lifecycle_release_service(pinned_service) when generation completes; ensure
you remove reliance on component->cancel_requested inside the long-running
generate paths and update generate* functions to accept a per-request cancel
token so concurrent generate calls don’t interfere.
| final archivePathPtr = archivePath.toNativeUtf8(); | ||
| final destPathPtr = destDir.toNativeUtf8(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart | sed -n '340,380p'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1667
🌐 Web query:
Dart package:ffi String.toNativeUtf8() default allocator malloc calloc memory deallocation
💡 Result:
String.toNativeUtf8()(frompackage:ffi/ffi.dart) allocates a new, NUL-terminated UTF-8 buffer on the native heap and returnsPointer<Utf8>. Its default allocator ismalloc:toNativeUtf8({Allocator allocator = malloc}). [1]- That memory is not GC-managed. Whoever allocates it must free it, using the corresponding allocator’s
free()(theAllocatorAPI isallocate(...)/free(pointer)). [2]
Practical rules
-
If you call
toNativeUtf8()and the C API only reads the string during the call, free it immediately after:import 'package:ffi/ffi.dart'; final p = 'hello'.toNativeUtf8(); // default: malloc [1] try { nativeFunc(p); } finally { malloc.free(p); // or: allocator.free(p) [2] }
-
If you pass
allocator: calloc(e.g., you want the buffer zero-initialized beyond the written bytes), then free with that allocator:final p = 'hello'.toNativeUtf8(allocator: calloc); try { nativeFunc(p); } finally { calloc.free(p); }
-
If the native side retains the pointer (stores it for later), you must define/know the ownership contract:
- Either Dart keeps ownership and frees it later, or
- The native library takes ownership and must provide a corresponding “free” API (recommended in general to avoid allocator-mismatch issues across libraries).
Notes
- The
ffipackage’s own example demonstrates that the pointer returned bytoNativeUtf8()must be freed after use. [3]
Sources: [1] StringUtf8Pointer.toNativeUtf8 API doc (default allocator = malloc), [2] dart:ffi Allocator API doc (allocate / free), [3] package:ffi example showing allocation + freeing of toNativeUtf8() result.
Fix allocator mismatch: toNativeUtf8() allocates with malloc but code frees with calloc.free().
String.toNativeUtf8() uses malloc by default. Freeing with calloc.free() is an allocator mismatch and corrupts native memory.
Use allocator: calloc to match the deallocator:
Fix
- final archivePathPtr = archivePath.toNativeUtf8();
- final destPathPtr = destDir.toNativeUtf8();
+ final archivePathPtr = archivePath.toNativeUtf8(allocator: calloc);
+ final destPathPtr = destDir.toNativeUtf8(allocator: calloc);Also applies to lines 369–370.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final archivePathPtr = archivePath.toNativeUtf8(); | |
| final destPathPtr = destDir.toNativeUtf8(); | |
| final archivePathPtr = archivePath.toNativeUtf8(allocator: calloc); | |
| final destPathPtr = destDir.toNativeUtf8(allocator: calloc); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`
around lines 351 - 352, The native string pointers archivePathPtr and
destPathPtr (and the other two pointers on lines 369–370) are created with
String.toNativeUtf8() which uses malloc by default but are later freed with
calloc.free(), causing an allocator mismatch; update the allocations to use the
calloc allocator by calling toNativeUtf8(allocator: calloc) for archivePath,
destDir, and the other two strings so the subsequent calloc.free() calls match
the allocator.
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart
Show resolved
Hide resolved
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart
Show resolved
Hide resolved
| static rac_result_t posixDeletePath(const char* path, int recursive, void* /*userData*/) { | ||
| if (!path) return RAC_ERROR_NULL_POINTER; | ||
|
|
||
| struct stat st; | ||
| if (stat(path, &st) != 0) { | ||
| return RAC_SUCCESS; // Already gone | ||
| } | ||
|
|
||
| if (S_ISDIR(st.st_mode)) { | ||
| if (recursive) { | ||
| // Recursively delete directory contents | ||
| DIR* dir = opendir(path); | ||
| if (!dir) return RAC_ERROR_FILE_IO; | ||
|
|
||
| struct dirent* entry; | ||
| while ((entry = readdir(dir)) != nullptr) { | ||
| if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { | ||
| continue; | ||
| } | ||
| std::string childPath = std::string(path) + "/" + entry->d_name; | ||
| posixDeletePath(childPath.c_str(), 1, nullptr); | ||
| } | ||
| closedir(dir); | ||
| } | ||
| return (rmdir(path) == 0) ? RAC_SUCCESS : RAC_ERROR_FILE_IO; | ||
| } else { | ||
| return (unlink(path) == 0) ? RAC_SUCCESS : RAC_ERROR_FILE_IO; | ||
| } |
There was a problem hiding this comment.
Don't follow symlinks during recursive deletes.
posixDeletePath() uses stat(), so a symlink inside a managed directory is treated as its target. That lets clearCache(), clearTemp(), or deleteModel() recurse outside the SDK storage tree and delete arbitrary files. Given the new native archive flow, downloaded content can realistically surface symlinks here.
🔒 Safer symlink handling
struct stat st;
- if (stat(path, &st) != 0) {
+ if (lstat(path, &st) != 0) {
return RAC_SUCCESS; // Already gone
}
+
+ if (S_ISLNK(st.st_mode)) {
+ return (unlink(path) == 0) ? RAC_SUCCESS : RAC_ERROR_FILE_IO;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 59 - 86, posixDeletePath currently uses stat which follows symlinks
and can recurse outside the intended tree; change the logic to use lstat when
checking the initial path and each child so symlinks are detected and not
followed. In the directory traversal inside posixDeletePath, for each entry
construct childPath, call lstat on childPath and if S_ISLNK skip it; if S_ISDIR
recurse into posixDeletePath, otherwise unlink regular files. Ensure you still
use rmdir/unlink return handling and avoid passing pointers to short-lived
strings into recursive calls by managing the childPath lifetime before calling
posixDeletePath.
|
@coderabbitai please do a detailed review. Thanks! |
|
@greptile please do a detailed review. Thanks! |
|
|
||
| // Download each file sequentially | ||
| bool any_failed = false; | ||
| for (size_t i = 0; i < file_count; ++i) { | ||
| const rac_model_file_descriptor_t& file = files[i]; | ||
|
|
||
| // Build full download URL | ||
| std::string file_url = std::string(base_download_url); | ||
| if (!file_url.empty() && file_url.back() != '/') file_url += "/"; | ||
| file_url += file.relative_path; | ||
|
|
||
| // Build destination path | ||
| std::string dest_path = std::string(model_folder); | ||
| if (file.destination_path && file.destination_path[0] != '\0') { | ||
| dest_path += "/" + std::string(file.destination_path); | ||
| } else { | ||
| dest_path += "/" + std::string(file.relative_path); | ||
| } | ||
|
|
||
| // Ensure parent directory exists | ||
| auto last_slash = dest_path.rfind('/'); | ||
| if (last_slash != std::string::npos) { | ||
| mkdir_p(dest_path.substr(0, last_slash).c_str()); | ||
| } | ||
|
|
||
| // For multi-file, download synchronously via platform adapter | ||
| // Update progress proportionally | ||
| double file_progress_start = static_cast<double>(i) / static_cast<double>(file_count); | ||
| double file_progress_end = | ||
| static_cast<double>(i + 1) / static_cast<double>(file_count); | ||
|
|
||
| // Update download manager with file-level progress | ||
| int64_t fake_downloaded = static_cast<int64_t>(file_progress_start * 100); | ||
| rac_download_manager_update_progress(dm_handle, task_id, fake_downloaded, 100); | ||
|
|
||
| // Start HTTP download for this file | ||
| // Note: Multi-file downloads are sequential — each file completes before next starts. | ||
| // For now we use the platform adapter's synchronous path. | ||
| char* http_task_id = nullptr; | ||
|
|
||
| // For multi-file we need a simpler blocking approach. | ||
| // Use shared_ptr to prevent double-free if callback fires unexpectedly. | ||
| struct multi_file_ctx { | ||
| bool completed; | ||
| rac_result_t result; | ||
| std::string downloaded_path; | ||
| }; | ||
|
|
||
| auto file_ctx = std::make_shared<multi_file_ctx>(multi_file_ctx{false, RAC_SUCCESS, ""}); | ||
|
|
||
| // Wrap in a raw holder for C callback void* — callback takes ownership of the holder | ||
| struct multi_file_holder { | ||
| std::shared_ptr<multi_file_ctx> ctx; | ||
| }; | ||
| auto* file_holder = new multi_file_holder{file_ctx}; | ||
|
|
||
| auto file_complete = [](rac_result_t result, const char* path, void* ud) { | ||
| auto* holder = static_cast<multi_file_holder*>(ud); | ||
| if (!holder || !holder->ctx) { | ||
| delete holder; | ||
| return; | ||
| } | ||
| holder->ctx->completed = true; | ||
| holder->ctx->result = result; | ||
| if (path) holder->ctx->downloaded_path = path; | ||
| delete holder; | ||
| }; | ||
|
|
||
| rac_result_t http_result = rac_http_download( | ||
| file_url.c_str(), dest_path.c_str(), nullptr /* no per-file progress */, file_complete, | ||
| file_holder, &http_task_id); | ||
|
|
||
| if (http_task_id) rac_free(http_task_id); | ||
|
|
||
| if (http_result != RAC_SUCCESS) { | ||
| if (file.is_required == RAC_TRUE) { | ||
| any_failed = true; | ||
| RAC_LOG_ERROR(LOG_TAG, "Required file download failed to start: %s", | ||
| file.relative_path); | ||
| // Download never started, so callback won't fire — delete holder safely | ||
| delete file_holder; | ||
| break; | ||
| } | ||
| RAC_LOG_WARNING(LOG_TAG, "Optional file download failed to start: %s", | ||
| file.relative_path); | ||
| // Download never started, so callback won't fire — delete holder safely | ||
| delete file_holder; | ||
| continue; | ||
| } | ||
|
|
||
| // Download started — async callback will delete file_holder | ||
| } | ||
|
|
||
| if (any_failed) { | ||
| rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED, | ||
| "One or more required files failed to download"); | ||
| } else { | ||
| // Update final progress | ||
| rac_download_manager_update_progress(dm_handle, task_id, 100, 100); | ||
| rac_download_manager_mark_complete(dm_handle, task_id, model_folder); | ||
| } | ||
|
|
||
| *out_task_id = task_id; | ||
| return RAC_SUCCESS; |
There was a problem hiding this comment.
Multi-file download marks task complete before transfers finish
rac_download_orchestrate_multi calls rac_http_download asynchronously (each call returns immediately with a task ID), but after the loop it calls rac_download_manager_mark_complete (or mark_failed) as if all transfers are done. In practice, the loop only verifies that each download started, not that it completed. The file_complete lambda sets file_ctx->completed = true and records the result, but nothing in the loop ever waits on that flag or checks file_ctx->result before advancing. As a result:
- The overall task is marked complete while files are still downloading in the background.
- Transfer-phase failures (network errors, partial data, etc.) inside the per-file callback are silently ignored —
any_failedis only set whenrac_http_download()itself returns an error at launch time, not when the transfer later fails.
The comment at line ~609 says "each file completes before next starts" and "synchronous path", but no synchronization primitive (semaphore, condition variable, or blocking join) is used to enforce this. The architecture as written is racing.
Consider introducing a synchronization mechanism (e.g., a semaphore or future/promise) to wait for each file's callback before proceeding to the next iteration, or restructure to chain completions properly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
Line: 574-677
Comment:
**Multi-file download marks task complete before transfers finish**
`rac_download_orchestrate_multi` calls `rac_http_download` asynchronously (each call returns immediately with a task ID), but after the loop it calls `rac_download_manager_mark_complete` (or `mark_failed`) as if all transfers are done. In practice, the loop only verifies that each download *started*, not that it *completed*. The `file_complete` lambda sets `file_ctx->completed = true` and records the result, but nothing in the loop ever waits on that flag or checks `file_ctx->result` before advancing. As a result:
1. The overall task is marked complete while files are still downloading in the background.
2. Transfer-phase failures (network errors, partial data, etc.) inside the per-file callback are silently ignored — `any_failed` is only set when `rac_http_download()` itself returns an error at launch time, not when the transfer later fails.
The comment at line ~609 says "each file completes before next starts" and "synchronous path", but no synchronization primitive (semaphore, condition variable, or blocking join) is used to enforce this. The architecture as written is racing.
Consider introducing a synchronization mechanism (e.g., a semaphore or future/promise) to wait for each file's callback before proceeding to the next iteration, or restructure to chain completions properly.
How can I resolve this? If you propose a fix, please make it concise.| if (any_failed) { | ||
| rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED, | ||
| "One or more required files failed to download"); | ||
| } else { | ||
| // Update final progress | ||
| rac_download_manager_update_progress(dm_handle, task_id, 100, 100); | ||
| rac_download_manager_mark_complete(dm_handle, task_id, model_folder); | ||
| } | ||
|
|
||
| *out_task_id = task_id; | ||
| return RAC_SUCCESS; |
There was a problem hiding this comment.
rac_download_orchestrate_multi returns RAC_SUCCESS on required-file failure
When a required file fails to start downloading (any_failed == true), the function still falls through to *out_task_id = task_id; return RAC_SUCCESS;. This means the caller receives a success code for an orchestration that already failed, making it impossible to distinguish a successful start from a failed one at the call site. The internal download manager state is correctly marked failed via rac_download_manager_mark_failed, but the function's own return value is misleading.
| if (any_failed) { | |
| rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED, | |
| "One or more required files failed to download"); | |
| } else { | |
| // Update final progress | |
| rac_download_manager_update_progress(dm_handle, task_id, 100, 100); | |
| rac_download_manager_mark_complete(dm_handle, task_id, model_folder); | |
| } | |
| *out_task_id = task_id; | |
| return RAC_SUCCESS; | |
| if (any_failed) { | |
| rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED, | |
| "One or more required files failed to download"); | |
| rac_free(task_id); | |
| return RAC_ERROR_DOWNLOAD_FAILED; | |
| } else { | |
| // Update final progress | |
| rac_download_manager_update_progress(dm_handle, task_id, 100, 100); | |
| rac_download_manager_mark_complete(dm_handle, task_id, model_folder); | |
| } | |
| *out_task_id = task_id; | |
| return RAC_SUCCESS; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
Line: 667-677
Comment:
**`rac_download_orchestrate_multi` returns `RAC_SUCCESS` on required-file failure**
When a required file fails to start downloading (`any_failed == true`), the function still falls through to `*out_task_id = task_id; return RAC_SUCCESS;`. This means the caller receives a success code for an orchestration that already failed, making it impossible to distinguish a successful start from a failed one at the call site. The internal download manager state is correctly marked failed via `rac_download_manager_mark_failed`, but the function's own return value is misleading.
```suggestion
if (any_failed) {
rac_download_manager_mark_failed(dm_handle, task_id, RAC_ERROR_DOWNLOAD_FAILED,
"One or more required files failed to download");
rac_free(task_id);
return RAC_ERROR_DOWNLOAD_FAILED;
} else {
// Update final progress
rac_download_manager_update_progress(dm_handle, task_id, 100, 100);
rac_download_manager_mark_complete(dm_handle, task_id, model_folder);
}
*out_task_id = task_id;
return RAC_SUCCESS;
```
How can I resolve this? If you propose a fix, please make it concise.| static const rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = { | ||
| RAC_TRUE, /* skip_macos_resources */ | ||
| RAC_FALSE, /* skip_symlinks */ | ||
| RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */ | ||
| }; |
There was a problem hiding this comment.
static const struct default in header creates one copy per translation unit
RAC_EXTRACTION_OPTIONS_DEFAULT is declared static const in a header. Every .cpp file that includes this header will get its own private copy of the struct (that's the intent of static), which is harmless but wasteful. More importantly, in C++ with C++20 (per CLAUDE.md) this pattern should use inline constexpr to give the variable external linkage and a single definition, matching the project's C++20 requirement and avoiding potential size regressions when the header is included by many translation units.
| static const rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = { | |
| RAC_TRUE, /* skip_macos_resources */ | |
| RAC_FALSE, /* skip_symlinks */ | |
| RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */ | |
| }; | |
| inline constexpr rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = { | |
| RAC_TRUE, /* skip_macos_resources */ | |
| RAC_FALSE, /* skip_symlinks */ | |
| RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */ | |
| }; |
Note: this is only valid in C++. Keep the static const form inside an #ifndef __cplusplus guard if C compatibility is needed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h
Line: 51-55
Comment:
**`static const` struct default in header creates one copy per translation unit**
`RAC_EXTRACTION_OPTIONS_DEFAULT` is declared `static const` in a header. Every `.cpp` file that includes this header will get its own private copy of the struct (that's the intent of `static`), which is harmless but wasteful. More importantly, in C++ with C++20 (per CLAUDE.md) this pattern should use `inline constexpr` to give the variable external linkage and a single definition, matching the project's C++20 requirement and avoiding potential size regressions when the header is included by many translation units.
```suggestion
inline constexpr rac_extraction_options_t RAC_EXTRACTION_OPTIONS_DEFAULT = {
RAC_TRUE, /* skip_macos_resources */
RAC_FALSE, /* skip_symlinks */
RAC_ARCHIVE_TYPE_NONE /* archive_type_hint */
};
```
Note: this is only valid in C++. Keep the `static const` form inside an `#ifndef __cplusplus` guard if C compatibility is needed.
How can I resolve this? If you propose a fix, please make it concise.|
@coderabbitai please do a detailed review. Thanks! |
|
I'll perform a thorough review across all changed files now. ✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
sdk/runanywhere-react-native/packages/core/package.json (1)
49-66:⚠️ Potential issue | 🟠 MajorUpdate documentation to reflect removal of react-native-zip-archive.
The package.json dependency cleanup is correct. However, the following documentation files still reference
react-native-zip-archiveand must be updated:
sdk/runanywhere-react-native/packages/core/README.md: Removereact-native-zip-archivefrom the installation instructionssdk/runanywhere-react-native/Docs/ARCHITECTURE.md: Update to reflect that archive extraction is now handled by native C++ libarchive implementation instead ofreact-native-zip-archiveStale documentation contradicts the dependency removal and will mislead users into installing an unnecessary package.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-react-native/packages/core/package.json` around lines 49 - 66, Update the two docs that still mention react-native-zip-archive: in README.md remove the package from installation/installation instructions and any example commands or yarn/npm entries, and in ARCHITECTURE.md replace references to react-native-zip-archive with a short note that archive extraction is now handled by the native C++ libarchive implementation (adjust any diagrams or text describing the extraction flow accordingly and remove any platform-specific setup steps for react-native-zip-archive).sdk/runanywhere-commons/src/features/platform/rac_backend_platform_register.cpp (1)
12-12: 🛠️ Refactor suggestion | 🟠 MajorUpdate coding guidelines to reflect C++20 standard requirement.
The project CMakeLists files uniformly set
CMAKE_CXX_STANDARD 20, confirming the C++20 upgrade is intentional across the codebase. However, the coding guidelines (CLAUDE.md) still specify "C++17 standard required" forsdk/runanywhere-commons/**/*.{cpp,hpp}. This inconsistency should be resolved by updating the documented standard to C++20 to maintain alignment between project documentation and actual build configuration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/platform/rac_backend_platform_register.cpp` at line 12, Update the coding guideline in CLAUDE.md that currently states "C++17 standard required" for the sdk/runanywhere-commons/**/*.{cpp,hpp} pattern to say "C++20 standard required" (to match project's CMAKE_CXX_STANDARD 20), and while editing also scan for any other references to "C++17" (or legacy CXX standard mentions) and update them to "C++20" so documentation and build config are consistent.sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp (2)
317-333:⚠️ Potential issue | 🟠 MajorInconsistent state if metadata file fails to open after index load.
If
index_.load()succeeds (line 321) but the metadata file fails to open (lines 330-332), the method returnsfalsewithout revertingindex_. This leaves the store in an inconsistent state:index_contains the newly loaded vectors whilechunks_andid_to_key_retain stale data from before the load.The fix on line 366 (
index_.clear()) handles JSON parse failures but doesn't cover the file-open failure path. This was flagged in the PR description as a known issue.Proposed fix: clear existing state before loading or revert on any failure
bool load(const std::string& path) { std::lock_guard<std::mutex> lock(mutex_); + // Store original state for rollback on partial failure + auto original_index_size = index_.size(); + // Load USearch index auto load_result = index_.load(path.c_str()); if (!load_result) { RAC_LOG_ERROR(LOG_TAG,"Failed to load USearch index: %s", load_result.error.what()); return false; } // Load metadata from JSON file std::string metadata_path = path + ".metadata.json"; std::ifstream metadata_file(metadata_path); if (!metadata_file) { RAC_LOG_ERROR(LOG_TAG,"Failed to open metadata file: %s", metadata_path.c_str()); + index_.clear(); // Revert index to consistent empty state return false; }Alternatively, clear all state upfront before loading:
bool load(const std::string& path) { std::lock_guard<std::mutex> lock(mutex_); + // Clear existing state before loading new data + index_.clear(); + chunks_.clear(); + id_to_key_.clear(); + next_key_ = 0; + // Load USearch index auto load_result = index_.load(path.c_str());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp` around lines 317 - 333, The load method can leave index_ populated while chunks_ and id_to_key_ remain stale if the metadata file fails to open; either clear state before attempting to load or revert on any failure: inside VectorStoreUSEarch::load (protecting with mutex_), clear index_ (index_.clear()), chunks_, and id_to_key_ before calling index_.load(path) or, if you prefer to load the index first, add a rollback path that calls index_.clear() and clears chunks_ and id_to_key_ whenever opening the metadata file or parsing JSON fails so the object never remains partially updated.
34-39:⚠️ Potential issue | 🔴 CriticalFix load() method state inconsistency and remove redundant macro definitions.
The
load()method has a critical bug: if the metadata file fails to open afterindex_.load()succeeds (lines 330–332), the method returns without clearing the index. This leaves the index with new data whilechunks_andid_to_key_retain old data, creating an inconsistent state. Theindex_.clear()at line 350 only executes in the catch block for JSON parse errors, not for file open failures.Additionally, lines 37–39 define
LOGI,LOGW,LOGEmacros that are never used; the code consistently callsRAC_LOG_ERROR,RAC_LOG_INFO, andRAC_LOG_WARNINGdirectly instead. Remove the unused macros.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp` around lines 34 - 39, The load() method currently leaves index_ populated when index_.load() succeeds but opening the metadata file fails; modify load() so that immediately after a failed metadata file open (the block after index_.load()), you call index_.clear() before returning and ensure chunks_ and id_to_key_ remain consistent (clear or reset them as currently done in the JSON parse catch path). Also remove the unused macro definitions LOGI/LOGW/LOGE (they are redundant because the code uses RAC_LOG_INFO/RAC_LOG_WARNING/RAC_LOG_ERROR directly) to clean up the header; keep references to index_.load(), index_.clear(), chunks_, id_to_key_, and the load() method to locate the changes.sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp (2)
63-77:⚠️ Potential issue | 🔴 CriticalMirror the new input guard in
transcribe_stream().This path still calls
convert_int16_to_float32()before validatingaudio_dataandaudio_size, soaudio_data == nullptrwith a non-zero size can still crash the process.Suggested fix
static rac_result_t whispercpp_stt_vtable_transcribe_stream(void* impl, const void* audio_data, size_t audio_size, const rac_stt_options_t* options, rac_stt_stream_callback_t callback, void* user_data) { + if (!audio_data || audio_size == 0 || (audio_size % sizeof(int16_t)) != 0) { + return RAC_ERROR_INVALID_ARGUMENT; + } + // Fall back to batch transcription rac_stt_result_t result = {}; std::vector<float> float_samples = convert_int16_to_float32(audio_data, audio_size);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp` around lines 63 - 77, The stream path in whispercpp_stt_vtable_transcribe_stream calls convert_int16_to_float32() before validating audio_data/audio_size and can crash if audio_data == nullptr with non-zero size; mirror the input guard used in transcribe_stream(): first check that audio_data is non-null and audio_size > 0 (and return the same error code used by transcribe_stream on invalid input), only then call convert_int16_to_float32(), and proceed to call rac_stt_whispercpp_transcribe and invoke the callback; ensure rac_stt_result_free(&result) still runs on the normal path.
62-78:⚠️ Potential issue | 🟠 MajorThis is still a batch fallback, not streaming transcription.
Buffering the full clip, running one batch decode, and emitting a single final callback does not provide streaming STT behavior. Callers using the streaming vtable still won't get incremental partials or low-latency updates.
Based on learnings Applies to
sdk/runanywhere-commons/src/backends/whispercpp/*.{cpp,h}: WhisperCPP backend must support GGML quantized models and implement streaming transcription.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cpp` around lines 62 - 78, The current whispercpp_stt_vtable_transcribe_stream implementation is only a batch fallback (buffers full clip, calls rac_stt_whispercpp_transcribe once, emits single final callback); replace it with a true streaming implementation that performs incremental chunked decoding and emits partial callbacks: add/init streaming APIs (e.g., whispercpp_stt_stream_init, whispercpp_stt_stream_process_chunk, whispercpp_stt_stream_finalize or reuse a whispercpp streaming decoder if available), convert and feed audio in small chunks (update convert_int16_to_float32 usage to support chunked conversion), call the callback with partial transcripts (callback(text, RAC_FALSE, user_data)) during processing and final (RAC_TRUE) on completion, and ensure GGML quantized model loading/flags are used in the stream init so quantized models are supported; finally update whispercpp_stt_vtable_transcribe_stream to delegate to the new streaming flow and free stream state on completion.sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp (1)
166-173:⚠️ Potential issue | 🟠 MajorRollback the tracker when outbound ID allocation fails.
Line 166 and Line 205 add the generation to
active_generationsbefore allocatingout_generation_id. Ifmallocreturns null, the call fails but the tracker remains orphaned in the map, and the caller never receives an ID that could later complete or fail that entry.Suggested fix
handle->active_generations[id] = tracker; // Allocate and copy the ID for the caller *out_generation_id = static_cast<char*>(malloc(id.size() + 1)); if (!*out_generation_id) { + handle->active_generations.erase(id); return RAC_ERROR_OUT_OF_MEMORY; } memcpy(*out_generation_id, id.c_str(), id.size() + 1);Also applies to: 205-212
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/llm/llm_analytics.cpp` around lines 166 - 173, The code inserts tracker into handle->active_generations before allocating *out_generation_id, which can orphan the tracker if malloc fails; either allocate and copy the ID (the malloc/memcpy for out_generation_id) before inserting tracker into handle->active_generations, or if you keep the current order then ensure you erase/remove the tracker from handle->active_generations on malloc failure (free any partially allocated resources and return RAC_ERROR_OUT_OF_MEMORY); apply this change to both places where tracker is inserted and out_generation_id is allocated (the two similar blocks handling tracker/out_generation_id).sdk/runanywhere-commons/src/features/llm/streaming_metrics.cpp (1)
1-10:⚠️ Potential issue | 🟡 MinorUpdate coding guidelines to reflect C++20 standard.
The actual C++ standard configured for
sdk/runanywhere-commonsis C++20 (set in CMakeLists.txt line 49), not C++17 as stated in the current coding guidelines. Update the guideline toC++20 standard requiredfor consistency with the actual build configuration across all submodules.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/llm/streaming_metrics.cpp` around lines 1 - 10, The repository's coding guidelines state C++17 but the project is built with C++20 (see CMakeLists.txt at the project root referencing C++20); update the guideline text to "C++20 standard required" wherever the coding standard is documented (e.g., CONTRIBUTING, README, docs or any coding-guidelines files) so it matches the actual build configuration used by sdk/runanywhere-commons and all submodules; ensure references in comments or headers (for example the file header in streaming_metrics.cpp) are also updated to reflect C++20.sdk/runanywhere-commons/src/core/rac_audio_utils.cpp (1)
184-193:⚠️ Potential issue | 🔴 CriticalAdd a separate
size_toverflow check before allocating.The new RIFF guard keeps the header field valid, but
wav_size = WAV_HEADER_SIZE + data_sizecan still wrap on 32-bit builds. If that happens,malloc()gets a tiny buffer and Line 205 copiespcm_sizebytes into it.🛠️ Suggested fix
+#include <limits> + // Guard against WAV header overflow: the RIFF chunk-size field (data_size + 36) // is uint32_t, so data_size must leave room for the 36-byte header overhead. if (pcm_size > static_cast<size_t>(UINT32_MAX) - (WAV_HEADER_SIZE - 8)) { return RAC_ERROR_INVALID_ARGUMENT; } + if (pcm_size > std::numeric_limits<size_t>::max() - WAV_HEADER_SIZE) { + return RAC_ERROR_INVALID_ARGUMENT; + } const uint32_t data_size = static_cast<uint32_t>(pcm_size);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/core/rac_audio_utils.cpp` around lines 184 - 193, The RIFF guard ensures the 32-bit header field fits but misses a platform-size overflow for wav_size; before computing wav_size or allocating, add a size_t overflow check such as verifying pcm_size <= SIZE_MAX - WAV_HEADER_SIZE (use SIZE_MAX) and return RAC_ERROR_INVALID_ARGUMENT if it would overflow, then compute wav_size = WAV_HEADER_SIZE + pcm_size (or WAV_HEADER_SIZE + data_size) and proceed to malloc; reference symbols: pcm_size, WAV_HEADER_SIZE, data_size, wav_size, and the allocation/copy path that follows.sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp (1)
152-152:⚠️ Potential issue | 🟠 MajorRequire or preserve
device_idduring auth-state swaps.
rac_auth_build_refresh_request()hard-fails withoutg_auth_state.device_id, but this path frees the old state and then treatsdevice_idas best-effort. If the response omits it orstr_dup(response->device_id)fails, we still return success and mark the session authenticated, but the next refresh cannot be built.🩹 Targeted fix
- if (!response || !response->access_token || !response->refresh_token) { + if (!response || !response->access_token || !response->refresh_token || + !response->device_id) { return -1; } // Pre-allocate required strings before modifying state char* new_access = str_dup(response->access_token); char* new_refresh = str_dup(response->refresh_token); - if (!new_access || !new_refresh) { + char* new_device = str_dup(response->device_id); + if (!new_access || !new_refresh || !new_device) { free(new_access); free(new_refresh); + free(new_device); return -1; } @@ - g_auth_state.device_id = str_dup(response->device_id); + g_auth_state.device_id = new_device; g_auth_state.user_id = str_dup(response->user_id); g_auth_state.organization_id = str_dup(response->organization_id);Also applies to: 168-175
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/network/auth_manager.cpp` at line 152, The code currently frees the old g_auth_state before swapping in the new tokens and treats response->device_id as optional, which breaks rac_auth_build_refresh_request() that requires device_id; change the swap logic so you require or preserve device_id: when creating the new state (the branch handling response->access_token/refresh_token), check response->device_id and attempt to strdup it into the new state's device_id; if response->device_id is NULL, copy the existing g_auth_state.device_id into the new state instead; if strdup fails for the new device_id, abort the swap and keep the existing g_auth_state (do not free it) and return an error; only mark the session authenticated after a successful device_id assignment—this touches g_auth_state, rac_auth_build_refresh_request(), response->device_id, and the code paths that set authenticated.sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp (2)
689-711:⚠️ Potential issue | 🟠 MajorGuard empty STT output here before invoking the LLM.
rac_voice_agent_process_voice_turn()already rejects!stt_result.text || strlen(stt_result.text) == 0, but this path forwardsstt_result.textdirectly torac_llm_component_generate(). A successful STT call with no text will now emit a transcription event and then hand a null/empty prompt to the LLM.🐛 Minimal fix
if (result != RAC_SUCCESS) { rac_voice_agent_event_t error_event = {}; error_event.type = RAC_VOICE_AGENT_EVENT_ERROR; error_event.data.error_code = result; callback(&error_event, user_data); return result; } + + if (!stt_result.text || strlen(stt_result.text) == 0) { + rac_stt_result_free(&stt_result); + rac_voice_agent_event_t error_event = {}; + error_event.type = RAC_VOICE_AGENT_EVENT_ERROR; + error_event.data.error_code = RAC_ERROR_INVALID_STATE; + callback(&error_event, user_data); + return RAC_ERROR_INVALID_STATE; + } // Emit transcription event rac_voice_agent_event_t transcription_event = {}; transcription_event.type = RAC_VOICE_AGENT_EVENT_TRANSCRIPTION; transcription_event.data.transcription = stt_result.text;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around lines 689 - 711, After emitting the transcription_event, guard against empty STT output before calling rac_llm_component_generate: check if stt_result.text is NULL or strlen(stt_result.text) == 0 and if so emit an error (or return a suitable rac_result_t) instead of invoking rac_llm_component_generate(handle->llm_handle, stt_result.text, ...); update the rac_voice_agent_process_voice_turn path to bail out when stt_result.text is empty so the LLM is never called with a null/empty prompt (use rac_stt_result_t stt_result, rac_voice_agent_event_t transcription_event, rac_llm_component_generate and callback to locate where to add the check).
668-788:⚠️ Potential issue | 🔴 CriticalDon't call
callback(...)while holdinghandle->mutex.Every callback in this function executes under the lock acquired at Line 669. A callback that re-enters
rac_voice_agent_cleanup(),rac_voice_agent_destroy(), or another mutex-takingrac_voice_agent_*API will deadlock the calling thread, and slow callbacks now block load/cleanup/destroy as well. Build/copy the event payloads under lock, then release the mutex before dispatch.As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around lines 668 - 788, The function currently calls callback(...) while holding handle->mutex which can deadlock; change the flow so you prepare and copy all event payloads (transcription text from stt_result, response text from llm_result, synthesized WAV bytes or a copied buffer) and any error codes while holding the lock, then release the lock and invoke callback for transcription, response, audio synthesized, and processed events; ensure you still call validation helpers (validate_all_components_ready), component callers (rac_stt_component_transcribe, rac_llm_component_generate, rac_tts_component_synthesize, rac_audio_float32_to_wav) and cleanup helpers (rac_stt_result_free, rac_llm_result_free, rac_tts_result_free) as before but only perform memory allocations/copies under the mutex and move all callback(...) invocations to after locking scope so callbacks run without holding handle->mutex.sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp (2)
118-153:⚠️ Potential issue | 🟠 MajorInitialize
out_resultbefore the new OOM exits.The added
RAC_ERROR_OUT_OF_MEMORYreturns at Lines 140-148 happen beforewords,num_words, and the timing fields are assigned. That leaves the result struct partially uninitialized on failure, which is risky if callers always pass it through a cleanup/helper path.🔧 Proposed fix
rac_result_t rac_stt_onnx_transcribe(rac_handle_t handle, const float* audio_samples, size_t num_samples, const rac_stt_options_t* options, rac_stt_result_t* out_result) { if (handle == nullptr || audio_samples == nullptr || out_result == nullptr) { return RAC_ERROR_NULL_POINTER; } + + out_result->text = nullptr; + out_result->detected_language = nullptr; + out_result->words = nullptr; + out_result->num_words = 0; + out_result->confidence = 0.0f; + out_result->processing_time_ms = 0.0; auto* h = static_cast<rac_onnx_stt_handle_impl*>(handle); if (!h->stt) { return RAC_ERROR_INVALID_HANDLE; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp` around lines 118 - 153, In rac_stt_onnx_transcribe, ensure the out_result struct is fully initialized before any early returns for OOM: set out_result->words = nullptr, out_result->num_words = 0, out_result->confidence = 1.0f, out_result->processing_time_ms = 0 (or a safe default) immediately after the null/handle checks and before allocating/strdup calls so that any return (e.g., the RAC_ERROR_OUT_OF_MEMORY paths around strdup for out_result->text and out_result->detected_language) leaves out_result in a consistent, initialized state; adjust the logic in rac_stt_onnx_transcribe accordingly (initialize out_result early, then perform strdup and handle freeing on failure).
218-230:⚠️ Potential issue | 🟡 MinorReturn
nullptrfor empty streaming transcripts too.
rac_stt_onnx_transcribe()maps empty text tonullptr, butrac_stt_onnx_decode_stream()currently allocates"". That gives batch and streaming callers different empty-result semantics for the same API surface.🔧 Proposed fix
auto result = h->stt->decode(stream_id); + if (result.text.empty()) { + *out_text = nullptr; + return RAC_SUCCESS; + } *out_text = strdup(result.text.c_str()); if (!*out_text) { return RAC_ERROR_OUT_OF_MEMORY; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/backends/onnx/rac_onnx.cpp` around lines 218 - 230, The function rac_stt_onnx_decode_stream currently strdup's an empty string which differs from rac_stt_onnx_transcribe's behavior; update rac_stt_onnx_decode_stream (and the local variables around h->stt->decode) to check if result.text.empty() and, if so, set *out_text = nullptr and return success, otherwise strdup result.text.c_str() and still check for allocation failure (RAC_ERROR_OUT_OF_MEMORY); ensure no memory is allocated for the empty case and maintain existing null-pointer checks at the top of the function.sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)
477-509:⚠️ Potential issue | 🟠 MajorTrack lifecycle state explicitly instead of deriving it from
is_initialized.
rac_vad_component_get_state()can only reportIDLEorLOADED, andrac_vad_component_get_metrics()fabricates load counts from that same boolean.start(),stop(),reset(), and cleanup transitions are never recorded, so callers cannot rely on this lifecycle API.Based on learnings "Component lifecycle implementations must support all rac_lifecycle_state_t transitions and track state changes".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 477 - 509, The component currently derives lifecycle from is_initialized causing rac_vad_component_get_state and rac_vad_component_get_metrics to be inaccurate; add an explicit lifecycle field (e.g., rac_lifecycle_state_t lifecycle_state) and counters (total_loads, successful_loads, failed_loads) to the rac_vad_component struct, update start(), stop(), reset(), and cleanup functions to set lifecycle_state on each transition and increment the appropriate counters, then change rac_vad_component_get_state() to return component->lifecycle_state and rac_vad_component_get_metrics() to copy the real counters into out_metrics while holding component->mtx and validating arguments.
♻️ Duplicate comments (13)
sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp (1)
240-261:⚠️ Potential issue | 🔴 Critical
rac_voice_agent_destroy()still tears down the handle while callers can be live.The mutex only protects threads that are already inside the critical section. Threads still queued on
handle->mutexcan wake after the lock drops, andrac_voice_agent_detect_speech()still bypasses that mutex entirely, so Line 261 can free the handle while other APIs still dereference it. Add a real shutdown barrier/in-flight counter and wait for it to drain before deleting the object.As per coding guidelines "Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp` around lines 240 - 261, rac_voice_agent_destroy() currently releases handle->mutex and immediately deletes handle while callers (e.g., rac_voice_agent_detect_speech) may still access it; add a proper shutdown barrier by introducing an in-flight counter and a shutdown flag on the handle (e.g., atomic<int> inflight_count and atomic<bool> shutting_down), incrementing/decrementing inflight_count in entry/exit paths of public APIs (including rac_voice_agent_detect_speech) and making rac_voice_agent_destroy() set shutting_down/is_configured=false, wait (spin or condition_variable) until inflight_count == 0, then proceed to destroy owned components and delete handle; update rac_voice_agent_detect_speech and other callers to check shutting_down/is_configured and return early while still correctly decrementing the counter so destroy can safely delete the handle.sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp (1)
189-192:⚠️ Potential issue | 🔴 CriticalAdvance
posbefore continuing on allocation failure.If either
strdupfails, the loop continues without advancingpos, causing an infinite retry of the same object. This turns an OOM condition into a hang.🐛 Proposed fix
if (!model->id || !model->name) { rac_model_info_free(model); + pos = obj_end; continue; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp` around lines 189 - 192, The loop over model entries is not advancing pos on allocation failure, causing an infinite retry; in the block that checks if (!model->id || !model->name) (after strdup failures), free the model (rac_model_info_free(model)) and then increment the loop index variable pos (or the iterator used to traverse entries) before continuing so the code advances to the next item instead of retrying the same one.sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp (1)
74-79:⚠️ Potential issue | 🔴 CriticalConstructor exceptions can still cross the
extern "C"boundary here.
new (std::nothrow)only converts allocation failure intonullptr; it does not swallow exceptions thrown while constructingrac_vad_analytics_s. If that constructor throws, this function still terminates the process instead of returning arac_result_t.In C++, does `new (std::nothrow) T(...)` suppress exceptions thrown by `T`'s constructor, or only allocation failures?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp` around lines 74 - 79, The allocation can still throw from the constructor of rac_vad_analytics_s; wrap the new (std::nothrow) rac_vad_analytics_s() call in a try/catch inside the function (the one that sets *out_handle) so constructor exceptions are caught, log the exception message (use exception.what()) and return a non-success rac_result_t instead of letting the exception cross the extern "C" boundary; also catch (...) for non-std::exception cases and return an appropriate error code. Ensure you still handle the nullptr case (allocation failure) as before, and reference the allocation and type name rac_vad_analytics_s and the output pointer *out_handle in your changes.sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (1)
4207-4239:⚠️ Potential issue | 🔴 CriticalRename these JNI exports to
native_bridge, notnative_1bridge.These symbols currently map to
com.runanywhere.sdk.native_bridge.RunAnywhereBridge, while the bridge class in this PR lives undercom.runanywhere.sdk.native.bridge.RunAnywhereBridge. The JVM will not find these methods, so every new archive/file-manager call here fails withUnsatisfiedLinkErrorat runtime.#!/bin/bash echo "=== New JNI exports ===" rg -n "Java_com_runanywhere_sdk_native(_1)?bridge_RunAnywhereBridge_(nativeExtractArchive|nativeDetectArchiveType|nativeFindModelPathAfterExtraction|nativeDownloadRequiresExtraction|nativeComputeDownloadDestination|nativeFileManager)" sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp echo echo "=== Kotlin package declaration ===" fd "RunAnywhereBridge.kt" sdk/runanywhere-kotlin -x sed -n '1,40p' {}Also applies to: 4245-4582
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 4207 - 4239, The JNI exported function names use "native_1bridge" but the Java/Kotlin class lives in package com.runanywhere.sdk.native.bridge, so rename the JNI symbols to use "native_bridge" (remove the "_1" escape) to match the JVM-mangled names; specifically update Java_com_runanywhere_sdk_native_1bridge_RunAnywhereBridge_nativeExtractArchive and Java_com_runanywhere_sdk_native_1bridge_RunAnywhereBridge_nativeDetectArchiveType (and the other related exports Java_com_runanywhere_sdk_native_1bridge_RunAnywhereBridge_nativeFindModelPathAfterExtraction, _nativeDownloadRequiresExtraction, _nativeComputeDownloadDestination, _nativeFileManager) to Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_<methodName> so the signatures match com.runanywhere.sdk.native.bridge.RunAnywhereBridge and the JVM can bind them. Ensure each renamed JNI function keeps the same parameters and return types and update any matching forward-declarations or references in the same file.sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart (1)
8-8:⚠️ Potential issue | 🔴 CriticalAdd the missing
dart:ioimport.
_ensureDirectoryExists()still instantiatesDirectory(path), so this file will not compile untildart:iois imported.Minimal fix
import 'dart:ffi'; +import 'dart:io';Run this to confirm the unresolved import:
#!/bin/bash sed -n '1,20p' sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart echo "---" rg -n '\bDirectory\s*\(' sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart echo "---" rg -n "^import 'dart:io';" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart || true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart` at line 8, The file is missing the dart:io import causing Directory usage in _ensureDirectoryExists() to fail; add import 'dart:io'; alongside the existing import 'package:runanywhere/native/dart_bridge_download.dart' so Directory(path) and related types resolve and the file will compile.sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)
184-185:⚠️ Potential issue | 🔴 CriticalThese getter locks still deadlock on callback re-entry.
rac_vad_component_process(),rac_vad_component_stop(), andrac_vad_component_cleanup()still holdcomponent->mtxwhile callingrac_energy_vad_*. That path can synchronously reachvad_speech_activity_callback()and user code, so any callback that queries init/state/metrics will re-enter these getters and block on the same mutex. Use atomics/snapshots for the read APIs, or releasecomponent->mtxbefore calling into the VAD engine.Also applies to: 482-503
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 184 - 185, The getters (eg. rac_vad_component_is_initialized) deadlock because component->mtx is held while code paths in rac_vad_component_process, rac_vad_component_stop, and rac_vad_component_cleanup call into rac_energy_vad_* which can synchronously re-enter vad_speech_activity_callback and user code that calls those getters; fix by removing long-held mutex usage: either make the state fields atomic or take a local snapshot of the fields under the lock and return the snapshot (no lock held during callbacks), and/or ensure rac_vad_component_process/stop/cleanup release component->mtx before calling rac_energy_vad_* so callbacks can safely call the getters without blocking. Target the functions rac_vad_component_process, rac_vad_component_stop, rac_vad_component_cleanup and the getter (is_initialized) to apply the atomic/snapshot change or to move the rac_energy_vad_* call outside the locked section.sdk/runanywhere-commons/src/server/http_server.cpp (1)
57-111:⚠️ Potential issue | 🔴 CriticalCatch exceptions before returning through
rac_server_start().
std::filesystem::exists,std::make_unique, andstd::threadcan still throw in this startup path, andrac_server_start()forwardsHttpServer::start()straight through the C API. Convert those failures to arac_result_tinstead of letting exceptions escape.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/server/http_server.cpp` around lines 57 - 111, Wrap the body of HttpServer::start (after acquiring the mutex_) in a try/catch that catches std::exception and ...; on exception return an appropriate rac_result_t (e.g., RAC_ERROR_INTERNAL) and log the exception message via RAC_LOG_ERROR; ensure any partially initialized members (server_, serverThread_, loaded model state) are cleaned/reset before returning—target the section that calls std::filesystem::exists, loadModel(modelPath_), std::make_unique<httplib::Server>(), setupCors(), setupRoutes(), and serverThread_ (std::thread(&HttpServer::serverThread, this)) so no C++ exception propagates out of HttpServer::start and all errors convert to rac_result_t.sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp (1)
59-86:⚠️ Potential issue | 🔴 CriticalDon't follow symlinks during recursive deletes.
stat()resolves the target before the type check here, so a symlink inside cache/temp/model storage can makeclearCache(),clearTemp(), ordeleteModel()recurse outside the managed tree. Uselstat()and unlink symlinks instead of descending into them.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp` around lines 59 - 86, The posixDeletePath function currently uses stat which follows symlinks and can recursively escape the managed tree; switch to lstat in posixDeletePath to detect S_ISLNK and treat symlinks as files (call unlink on the path and return appropriate RAC_SUCCESS/RAC_ERROR_FILE_IO) instead of descending into them; additionally, when iterating directory entries in posixDeletePath, call lstat on each childPath (not stat) and if S_ISLNK unlink the child, if S_ISDIR recurse (with the same lstat-based checks), otherwise unlink the child; ensure DIR* is closed on all paths and propagate error codes from unlink/rmdir calls consistently.sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp (1)
37-55:⚠️ Potential issue | 🔴 CriticalBackslash and Windows paths still bypass the zip-slip checks.
is_path_safe()only blocks/-rooted and/../forms, and hardlink targets are rewritten without the same validation. Entries like..\payload,C:\payload, or\\server\sharecan still escapedestination_diron Windows or via link targets.Also applies to: 217-245
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp` around lines 37 - 55, is_path_safe currently only checks POSIX-style absolute and ".." components and misses Windows-style backslashes, drive letters and UNC paths, and hardlink target validation; update is_path_safe to normalize or reject backslashes by treating '\' as a path separator, reject Windows absolute forms (leading drive letter like "C:" or leading "\\" for UNC), normalize separators and collapse/sanitize "." and ".." components before acceptance, and apply the same validation to any hardlink target rewriting logic (the function/logic that rewrites hardlink targets mentioned around lines 217-245). Ensure you reference and modify is_path_safe and the hardlink-target rewrite code so both use the same normalized-path validation routine.sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart (2)
364-367:⚠️ Potential issue | 🔴 CriticalThrow on native extraction failure instead of returning the archive path.
Returning
archivePathkeeps the caller on the success path: it deletes the archive, updates the registry, and emits completion for an unextracted model.Fix
if (result != 0) { - _logger.error('Native extraction failed with code: $result'); - return archivePath; + throw Exception( + 'Native extraction failed with code: $result for $archivePath', + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart` around lines 364 - 367, The code currently logs an extraction error and returns archivePath which makes the caller proceed as if extraction succeeded; instead, in the extraction routine where you check "if (result != 0)" (using variables result, archivePath and logger _logger.error), throw a descriptive exception (including result and archivePath) rather than returning archivePath so the caller will treat it as a failure and will not delete the archive or update the registry/emit completion for an unextracted model.
351-352:⚠️ Potential issue | 🔴 CriticalMatch
toNativeUtf8()with the allocator you free.
toNativeUtf8()usesmallocby default, so freeing these pointers withcalloc.free()is an allocator mismatch and can corrupt native memory.Fix
- final archivePathPtr = archivePath.toNativeUtf8(); - final destPathPtr = destDir.toNativeUtf8(); + final archivePathPtr = archivePath.toNativeUtf8(allocator: calloc); + final destPathPtr = destDir.toNativeUtf8(allocator: calloc);Also applies to: 369-370
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart` around lines 351 - 352, toNativeUtf8() defaults to malloc, but the code frees the returned pointers with calloc.free(), causing an allocator mismatch; for archivePathPtr and destPathPtr (and the other pointers at the same pattern around the later block) either pass allocator: calloc to toNativeUtf8(…) or change the frees to use malloc.free(…) so allocator used to allocate matches the one used to free; update the calls around archivePathPtr and destPathPtr (and the matching pointers at the later occurrence) accordingly and ensure the proper allocator import is available.sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_download.dart (1)
148-158:⚠️ Potential issue | 🔴 CriticalMatch the allocator used by
toNativeUtf8().These helpers allocate UTF-8 inputs with
malloc(the default fortoNativeUtf8()), but free them withcalloc.free(). That is allocator-mismatched native memory corruption on every call path.Fix
- final dirPtr = extractedDir.toNativeUtf8(); + final dirPtr = extractedDir.toNativeUtf8(allocator: calloc); ... - final urlPtr = url.toNativeUtf8(); + final urlPtr = url.toNativeUtf8(allocator: calloc); ... - final modelIdPtr = modelId.toNativeUtf8(); - final urlPtr = downloadUrl.toNativeUtf8(); + final modelIdPtr = modelId.toNativeUtf8(allocator: calloc); + final urlPtr = downloadUrl.toNativeUtf8(allocator: calloc);Also applies to: 176-181, 207-224
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_download.dart` around lines 148 - 158, The UTF-8 pointer from extractedDir.toNativeUtf8() is allocated with malloc but is being freed with calloc.free(), causing allocator mismatch; fix by matching allocators consistently—either allocate dirPtr with the same allocator as outPath (call extractedDir.toNativeUtf8(allocator: calloc)) or free dirPtr with malloc.free() instead of calloc.free(); apply the same fix to the other occurrences referenced (around the blocks for lines ~176-181 and ~207-224) so every toNativeUtf8() allocation uses the same allocator as its corresponding free.sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp (1)
575-678:⚠️ Potential issue | 🔴 CriticalRace condition in
rac_download_orchestrate_multi()— downloads complete prematurely.This issue was flagged in a previous review and remains unresolved. The code comments claim "sequential — each file completes before next starts" (lines 610-611), but
rac_http_downloadis asynchronous and returns immediately after starting the download.The current flow:
- Loop starts HTTP download for file
irac_http_downloadreturnsRAC_SUCCESS(download started, not completed)- Loop immediately proceeds to file
i+1- After all downloads are started, lines 667-674 mark the task complete
This means
rac_download_manager_mark_completeis called while downloads are still in flight, and failures in required files won't be detected.To fix: After
rac_http_downloadreturns success, poll/wait forfile_ctx->completedbefore proceeding to the next file, then checkfile_ctx->resultto detect failures.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp` around lines 575 - 678, After starting each async download with rac_http_download (in rac_download_orchestrate_multi), wait for that file's completion by polling the shared file_ctx->completed flag (the shared_ptr created as file_ctx) before continuing to the next iteration; once completed check file_ctx->result and if it is not RAC_SUCCESS treat required files (file.is_required) as failures (set any_failed, log via RAC_LOG_ERROR/RAC_LOG_WARNING) and break/continue accordingly, and only call rac_download_manager_mark_complete or rac_download_manager_mark_failed after all file results are observed; do not delete file_holder after a successful start (the file_complete callback owns and deletes it), and update per-file progress via rac_download_manager_update_progress based on file_progress_start/file_progress_end before/after the wait.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 14669245-45cb-41f7-8936-f210b100a466
⛔ Files ignored due to path filters (3)
examples/react-native/RunAnywhereAI/package-lock.jsonis excluded by!**/package-lock.jsonexamples/react-native/RunAnywhereAI/yarn.lockis excluded by!**/yarn.lock,!**/*.locksdk/runanywhere-react-native/yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (141)
Package.resolvedPackage.swiftPlayground/README.mdPlayground/linux-voice-assistant/CMakeLists.txtPlayground/linux-voice-assistant/README.mdPlayground/openclaw-hybrid-assistant/CMakeLists.txtexamples/flutter/RunAnywhereAI/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.javaexamples/flutter/RunAnywhereAI/ios/Runner/GeneratedPluginRegistrant.mexamples/ios/RunAnywhereAI/RunAnywhereAI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedsdk/runanywhere-commons/.clang-formatsdk/runanywhere-commons/CLAUDE.mdsdk/runanywhere-commons/CMakeLists.txtsdk/runanywhere-commons/README.mdsdk/runanywhere-commons/VERSIONSsdk/runanywhere-commons/exports/RACommons.exportssdk/runanywhere-commons/include/rac/core/rac_logger.hsdk/runanywhere-commons/include/rac/infrastructure/download/rac_download.hsdk/runanywhere-commons/include/rac/infrastructure/download/rac_download_orchestrator.hsdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.hsdk/runanywhere-commons/include/rac/infrastructure/file_management/rac_file_manager.hsdk/runanywhere-commons/src/backends/llamacpp/CMakeLists.txtsdk/runanywhere-commons/src/backends/llamacpp/jni/rac_backend_llamacpp_jni.cppsdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cppsdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.hsdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cppsdk/runanywhere-commons/src/backends/llamacpp/rac_vlm_llamacpp.cppsdk/runanywhere-commons/src/backends/onnx/CMakeLists.txtsdk/runanywhere-commons/src/backends/onnx/jni/rac_backend_onnx_jni.cppsdk/runanywhere-commons/src/backends/onnx/onnx_backend.cppsdk/runanywhere-commons/src/backends/onnx/onnx_backend.hsdk/runanywhere-commons/src/backends/onnx/rac_backend_onnx_register.cppsdk/runanywhere-commons/src/backends/onnx/rac_onnx.cppsdk/runanywhere-commons/src/backends/whispercpp/CMakeLists.txtsdk/runanywhere-commons/src/backends/whispercpp/jni/rac_backend_whispercpp_jni.cppsdk/runanywhere-commons/src/backends/whispercpp/rac_backend_whispercpp_register.cppsdk/runanywhere-commons/src/backends/whispercpp/rac_stt_whispercpp.cppsdk/runanywhere-commons/src/backends/whispercpp/whispercpp_backend.cppsdk/runanywhere-commons/src/backends/whisperkit_coreml/CMakeLists.txtsdk/runanywhere-commons/src/backends/whisperkit_coreml/rac_backend_whisperkit_coreml_register.cppsdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cppsdk/runanywhere-commons/src/core/rac_audio_utils.cppsdk/runanywhere-commons/src/core/rac_core.cppsdk/runanywhere-commons/src/core/rac_error.cppsdk/runanywhere-commons/src/core/rac_logger.cppsdk/runanywhere-commons/src/core/rac_structured_error.cppsdk/runanywhere-commons/src/features/diffusion/diffusion_component.cppsdk/runanywhere-commons/src/features/llm/llm_analytics.cppsdk/runanywhere-commons/src/features/llm/llm_component.cppsdk/runanywhere-commons/src/features/llm/streaming_metrics.cppsdk/runanywhere-commons/src/features/llm/tool_calling.cppsdk/runanywhere-commons/src/features/platform/rac_backend_platform_register.cppsdk/runanywhere-commons/src/features/rag/CMakeLists.txtsdk/runanywhere-commons/src/features/rag/jni/rac_rag_jni.cppsdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cppsdk/runanywhere-commons/src/features/rag/vector_store_usearch.cppsdk/runanywhere-commons/src/features/stt/stt_analytics.cppsdk/runanywhere-commons/src/features/stt/stt_component.cppsdk/runanywhere-commons/src/features/tts/tts_analytics.cppsdk/runanywhere-commons/src/features/tts/tts_component.cppsdk/runanywhere-commons/src/features/vad/energy_vad.cppsdk/runanywhere-commons/src/features/vad/vad_analytics.cppsdk/runanywhere-commons/src/features/vad/vad_component.cppsdk/runanywhere-commons/src/features/vlm/vlm_component.cppsdk/runanywhere-commons/src/features/voice_agent/voice_agent.cppsdk/runanywhere-commons/src/features/wakeword/wakeword_service.cppsdk/runanywhere-commons/src/infrastructure/download/download_manager.cppsdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cppsdk/runanywhere-commons/src/infrastructure/events/event_publisher.cppsdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cppsdk/runanywhere-commons/src/infrastructure/file_management/file_manager.cppsdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cppsdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cppsdk/runanywhere-commons/src/infrastructure/model_management/model_paths.cppsdk/runanywhere-commons/src/infrastructure/model_management/model_registry.cppsdk/runanywhere-commons/src/infrastructure/model_management/model_strategy.cppsdk/runanywhere-commons/src/infrastructure/network/api_types.cppsdk/runanywhere-commons/src/infrastructure/network/auth_manager.cppsdk/runanywhere-commons/src/infrastructure/network/http_client.cppsdk/runanywhere-commons/src/infrastructure/storage/storage_analyzer.cppsdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cppsdk/runanywhere-commons/src/jni/CMakeLists.txtsdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppsdk/runanywhere-commons/src/server/CMakeLists.txtsdk/runanywhere-commons/src/server/http_server.cppsdk/runanywhere-commons/src/server/http_server.hsdk/runanywhere-commons/src/server/openai_handler.cppsdk/runanywhere-commons/src/utils/rac_image_utils.cppsdk/runanywhere-commons/tests/CMakeLists.txtsdk/runanywhere-commons/tests/test_download_orchestrator.cppsdk/runanywhere-commons/tests/test_extraction.cppsdk/runanywhere-commons/tools/CMakeLists.txtsdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dartsdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/file_management/services/simplified_file_manager.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_download.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-flutter/packages/runanywhere/pubspec.yamlsdk/runanywhere-flutter/packages/runanywhere_onnx/lib/onnx_download_strategy.dartsdk/runanywhere-flutter/packages/runanywhere_onnx/pubspec.yamlsdk/runanywhere-kotlin/build.gradle.ktssdk/runanywhere-kotlin/modules/runanywhere-core-onnx/build.gradle.ktssdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.ktsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeFileManager.ktsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeModelRegistry.ktsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.ktsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+ModelManagement.jvmAndroid.ktsdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+Storage.jvmAndroid.ktsdk/runanywhere-react-native/packages/core/android/CMakeLists.txtsdk/runanywhere-react-native/packages/core/android/build.gradlesdk/runanywhere-react-native/packages/core/android/consumer-rules.prosdk/runanywhere-react-native/packages/core/android/src/main/cpp/cpp-adapter.cppsdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/ArchiveUtility.ktsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cppsdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cppsdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.hppsdk/runanywhere-react-native/packages/core/ios/ArchiveUtility.swiftsdk/runanywhere-react-native/packages/core/ios/ArchiveUtilityBridge.msdk/runanywhere-react-native/packages/core/package.jsonsdk/runanywhere-react-native/packages/core/src/Public/Extensions/RunAnywhere+Storage.tssdk/runanywhere-react-native/packages/core/src/services/FileSystem.tssdk/runanywhere-react-native/packages/core/src/services/index.tssdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_download_orchestrator.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_file_manager.hsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Download.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+FileManager.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+ModelRegistry.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Storage.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService+Execution.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/AlamofireDownloadService.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Services/ExtractionService.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Utilities/ArchiveUtility.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Services/SimplifiedFileManager.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Utilities/FileOperationsUtilities.swiftsdk/runanywhere-web/packages/core/src/Infrastructure/ArchiveUtility.tssdk/runanywhere-web/packages/llamacpp/src/Foundation/PlatformAdapter.tssdk/runanywhere-web/wasm/CMakeLists.txt
💤 Files with no reviewable changes (12)
- sdk/runanywhere-react-native/packages/core/ios/ArchiveUtilityBridge.m
- Package.swift
- sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/ArchiveUtility.kt
- sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/build.gradle.kts
- sdk/runanywhere-flutter/packages/runanywhere_onnx/pubspec.yaml
- sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Download/Utilities/ArchiveUtility.swift
- sdk/runanywhere-react-native/packages/core/src/services/index.ts
- Package.resolved
- sdk/runanywhere-react-native/packages/core/android/build.gradle
- sdk/runanywhere-react-native/packages/core/ios/ArchiveUtility.swift
- sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml
- sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/FileManagement/Utilities/FileOperationsUtilities.swift
| const size_t num_samples = pcm_size / sizeof(float); | ||
|
|
||
| // Guard against WAV header overflow: data_size field is uint32_t (max ~4GB) | ||
| if (num_samples > UINT32_MAX / sizeof(int16_t)) { | ||
| return RAC_ERROR_INVALID_ARGUMENT; | ||
| } | ||
|
|
||
| // Int16 data size (2 bytes per sample) | ||
| const uint32_t int16_data_size = static_cast<uint32_t>(num_samples * 2); | ||
| const uint32_t int16_data_size = static_cast<uint32_t>(num_samples * sizeof(int16_t)); | ||
|
|
||
| // Total WAV file size | ||
| const size_t wav_size = WAV_HEADER_SIZE + int16_data_size; |
There was a problem hiding this comment.
Fix the float32 upper bound; it still permits RIFF-size overflow.
This check only guarantees the data chunk fits in uint32_t. build_wav_header() writes data_size + 36, so large inputs can still overflow the RIFF chunk size while passing validation here. wav_size = WAV_HEADER_SIZE + int16_data_size also remains free to wrap on 32-bit size_t.
🛠️ Suggested fix
+#include <limits>
+
- // Guard against WAV header overflow: data_size field is uint32_t (max ~4GB)
- if (num_samples > UINT32_MAX / sizeof(int16_t)) {
+ // Leave room for the RIFF chunk-size field: data_size + 36 must fit in uint32_t.
+ constexpr size_t kMaxRiffDataSize =
+ static_cast<size_t>(UINT32_MAX) - (WAV_HEADER_SIZE - 8);
+ if (num_samples > kMaxRiffDataSize / sizeof(int16_t)) {
return RAC_ERROR_INVALID_ARGUMENT;
}
// Int16 data size (2 bytes per sample)
const uint32_t int16_data_size = static_cast<uint32_t>(num_samples * sizeof(int16_t));
// Total WAV file size
+ if (int16_data_size > std::numeric_limits<size_t>::max() - WAV_HEADER_SIZE) {
+ return RAC_ERROR_INVALID_ARGUMENT;
+ }
const size_t wav_size = WAV_HEADER_SIZE + int16_data_size;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/core/rac_audio_utils.cpp` around lines 125 - 136,
The current check only ensures int16_data_size fits in uint32_t but misses that
build_wav_header() adds 36 to form the RIFF chunk size and that wav_size can
wrap on 32-bit size_t; change the validation to compute int16_data_size as
before and then assert (uint64_t)int16_data_size + (uint64_t)36 <= UINT32_MAX to
prevent RIFF-size overflow, and compute wav_size using a wider type (e.g.,
uint64_t or size_t after checking) while also verifying the final wav_size does
not exceed SIZE_MAX before casting back; update the guard around
num_samples/int16_data_size, and reference WAV_HEADER_SIZE and
build_wav_header() in the check to ensure both the data chunk and RIFF chunk
sizes are validated.
| // Acquire lock only for state reads, release before long-running transcription | ||
| std::string transcription_id = generate_unique_id(); | ||
| const char* model_id = rac_lifecycle_get_model_id(component->lifecycle); | ||
| const char* model_name = rac_lifecycle_get_model_name(component->lifecycle); | ||
| rac_handle_t service = nullptr; | ||
| rac_stt_options_t local_options; | ||
| rac_inference_framework_t framework; | ||
| int32_t sample_rate = 0; | ||
| const char* model_id = nullptr; | ||
| const char* model_name = nullptr; | ||
|
|
||
| // Debug: Log if model_id is null | ||
| if (!model_id) { | ||
| log_warning( | ||
| "STT.Component", | ||
| "rac_lifecycle_get_model_id returned null - model_id may not be set in telemetry"); | ||
| } else { | ||
| log_debug("STT.Component", "STT transcription using model_id: %s", model_id); | ||
| { | ||
| std::lock_guard<std::mutex> lock(component->mtx); | ||
|
|
||
| model_id = rac_lifecycle_get_model_id(component->lifecycle); | ||
| model_name = rac_lifecycle_get_model_name(component->lifecycle); | ||
| framework = component->actual_framework; | ||
| sample_rate = component->config.sample_rate; | ||
|
|
||
| // Copy effective options to local so we can release the lock | ||
| local_options = options ? *options : component->default_options; | ||
|
|
||
| rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service); | ||
| if (result != RAC_SUCCESS) { | ||
| log_error("STT.Component", "No model loaded - cannot transcribe"); | ||
|
|
||
| // Emit transcription failed event | ||
| rac_analytics_event_data_t event = {}; | ||
| event.type = RAC_EVENT_STT_TRANSCRIPTION_FAILED; | ||
| event.data.stt_transcription = RAC_ANALYTICS_STT_TRANSCRIPTION_DEFAULT; | ||
| event.data.stt_transcription.transcription_id = transcription_id.c_str(); | ||
| event.data.stt_transcription.model_id = model_id; | ||
| event.data.stt_transcription.model_name = model_name; | ||
| event.data.stt_transcription.error_code = result; | ||
| event.data.stt_transcription.error_message = "No model loaded"; | ||
| rac_analytics_event_emit(RAC_EVENT_STT_TRANSCRIPTION_FAILED, &event); | ||
|
|
||
| return result; | ||
| } | ||
| } | ||
| // Lock released — safe to do long-running transcription |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find lifecycle header/source files to understand API contracts
fd -e h -e hpp -e c -e cpp 'lifecycle' sdk/runanywhere-commons | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 211
🏁 Script executed:
# Look for rac_lifecycle function definitions and documentation
rg -n 'rac_lifecycle_require_service|rac_lifecycle_get_model_id|rac_lifecycle_get_model_name' sdk/runanywhere-commons -A 3Repository: RunanywhereAI/runanywhere-sdks
Length of output: 15614
🏁 Script executed:
# Check for any retain/release/pin mechanisms in lifecycle layer
rg -n 'retain|release|refcount|pin|acquire' sdk/runanywhere-commons/src -i | grep -i lifecycle | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 542
🏁 Script executed:
# Get the full transcribe function to see all uses of borrowed state after unlock (lines 327-445)
sed -n '327,445p' sdk/runanywhere-commons/src/features/stt/stt_component.cpp | cat -nRepository: RunanywhereAI/runanywhere-sdks
Length of output: 6701
🏁 Script executed:
# Get full rac_lifecycle_get_model_id and rac_lifecycle_get_model_name implementations
sed -n '314,340p' sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp | cat -nRepository: RunanywhereAI/runanywhere-sdks
Length of output: 957
🏁 Script executed:
# Get full rac_lifecycle_require_service implementation
sed -n '354,375p' sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp | cat -nRepository: RunanywhereAI/runanywhere-sdks
Length of output: 961
🏁 Script executed:
# Look at the LifecycleManager class definition to understand data storage and thread safety
rg -n 'class LifecycleManager|struct LifecycleManager' sdk/runanywhere-commons -A 30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 3433
🏁 Script executed:
# Check for concurrent access to model_id and model_name during unload/destroy
rg -n 'model_id_|model_name_|current_service' sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp | head -30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 825
Keep lifecycle-owned state alive for the entire transcription.
The model_id and model_name pointers captured at lines 339–340 are borrowed directly from LifecycleManager's internal std::string storage (via c_str()), and the lock is released immediately. These pointers are then used through line 407 in rac_stt_transcribe() and multiple telemetry events. Similarly, the service handle from rac_lifecycle_require_service() is a raw, unowned pointer with no reference counting. Concurrent calls to rac_lifecycle_unload() or rac_lifecycle_reset() can clear or modify those std::string fields, causing use-after-free. Copy model_id and model_name to owned std::string variables before unlocking, and either hold the mutex for the full operation or implement explicit pinning for the service handle per the component lifecycle contract.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/features/stt/stt_component.cpp` around lines 327
- 365, The code currently stores raw pointers model_id and model_name (from
rac_lifecycle_get_model_id/get_model_name) and a raw service handle (from
rac_lifecycle_require_service) then releases the lock, which can lead to
use-after-free; fix by copying the model strings into owned std::string locals
(e.g., local_model_id/local_model_name) while still under component->mtx and
keep using those owned strings after unlock, and ensure the service handle is
explicitly pinned/retained for the duration of rac_stt_transcribe (either by
using the lifecycle's pin/retain API or by holding the mutex for the full
transcription) and then release/unpin the service when done (adjust code around
rac_lifecycle_require_service, rac_stt_transcribe, and the telemetry emission
paths to use the owned strings and correctly release the service).
| const char* voice_id = nullptr; | ||
| const char* voice_name = nullptr; | ||
|
|
||
| // Debug: Log if voice_id is null | ||
| if (!voice_id) { | ||
| log_warning("TTS.Component", | ||
| "rac_lifecycle_get_model_id returned null - voice may not be set in telemetry"); | ||
| } else { | ||
| log_debug("TTS.Component", "TTS synthesis using voice_id: %s", voice_id); | ||
| { | ||
| std::lock_guard<std::mutex> lock(component->mtx); | ||
|
|
||
| voice_id = rac_lifecycle_get_model_id(component->lifecycle); | ||
| voice_name = rac_lifecycle_get_model_name(component->lifecycle); | ||
| framework = component->actual_framework; | ||
|
|
||
| // Copy effective options to local so we can release the lock | ||
| local_options = options ? *options : component->default_options; | ||
|
|
||
| rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service); | ||
| if (result != RAC_SUCCESS) { | ||
| log_error("TTS.Component", "No voice loaded - cannot synthesize"); | ||
| // Emit SYNTHESIS_FAILED event | ||
| rac_analytics_event_data_t event_data; | ||
| event_data.data.tts_synthesis = RAC_ANALYTICS_TTS_SYNTHESIS_DEFAULT; | ||
| event_data.data.tts_synthesis.synthesis_id = synthesis_id.c_str(); | ||
| event_data.data.tts_synthesis.model_id = voice_id; | ||
| event_data.data.tts_synthesis.model_name = voice_name; | ||
| event_data.data.tts_synthesis.framework = framework; | ||
| event_data.data.tts_synthesis.error_code = result; | ||
| event_data.data.tts_synthesis.error_message = "No voice loaded"; | ||
| rac_analytics_event_emit(RAC_EVENT_TTS_SYNTHESIS_FAILED, &event_data); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Dangling pointer risk: voice_id and voice_name used after lock release.
The pointers voice_id and voice_name are obtained from rac_lifecycle_get_model_id/name() inside the lock, but they're used after the lock is released (lines 365-367, 389-390, etc.). If another thread calls rac_tts_component_load_voice() or rac_tts_component_unload() concurrently, these pointers could become dangling.
Copy to std::string before releasing the lock:
🔒 Proposed fix to copy strings before releasing lock
std::string synthesis_id = generate_uuid_v4();
rac_handle_t service = nullptr;
rac_tts_options_t local_options;
rac_inference_framework_t framework;
- const char* voice_id = nullptr;
- const char* voice_name = nullptr;
+ std::string voice_id_str;
+ std::string voice_name_str;
{
std::lock_guard<std::mutex> lock(component->mtx);
- voice_id = rac_lifecycle_get_model_id(component->lifecycle);
- voice_name = rac_lifecycle_get_model_name(component->lifecycle);
+ const char* vid = rac_lifecycle_get_model_id(component->lifecycle);
+ const char* vname = rac_lifecycle_get_model_name(component->lifecycle);
+ voice_id_str = vid ? vid : "";
+ voice_name_str = vname ? vname : "";
framework = component->actual_framework;Then use voice_id_str.c_str() and voice_name_str.c_str() in the event emissions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/features/tts/tts_component.cpp` around lines 330
- 357, The code obtains voice_id and voice_name via rac_lifecycle_get_model_id /
rac_lifecycle_get_model_name while holding component->mtx but then uses those
raw const char* values after releasing the lock, risking dangling pointers; fix
by copying the strings into std::string locals (e.g., voice_id_str,
voice_name_str) while still inside the lock, and later use voice_id_str.c_str()
and voice_name_str.c_str() wherever voice_id and voice_name are currently used
(including in rac_analytics_event_data_t population and any logging), ensuring
no raw lifecycle pointers escape the locked region.
| const char* voice_id = nullptr; | ||
| const char* voice_name = nullptr; | ||
| int32_t char_count = static_cast<int32_t>(std::strlen(text)); | ||
|
|
||
| { | ||
| std::lock_guard<std::mutex> lock(component->mtx); | ||
|
|
||
| voice_id = rac_lifecycle_get_model_id(component->lifecycle); | ||
| voice_name = rac_lifecycle_get_model_name(component->lifecycle); | ||
| framework = component->actual_framework; | ||
|
|
||
| // Copy effective options to local so we can release the lock | ||
| local_options = options ? *options : component->default_options; | ||
|
|
||
| rac_result_t result = rac_lifecycle_require_service(component->lifecycle, &service); | ||
| if (result != RAC_SUCCESS) { | ||
| log_error("TTS.Component", "No voice loaded - cannot synthesize stream"); | ||
| // Emit SYNTHESIS_FAILED event | ||
| rac_analytics_event_data_t event_data; | ||
| event_data.data.tts_synthesis = RAC_ANALYTICS_TTS_SYNTHESIS_DEFAULT; | ||
| event_data.data.tts_synthesis.synthesis_id = synthesis_id.c_str(); | ||
| event_data.data.tts_synthesis.model_id = voice_id; | ||
| event_data.data.tts_synthesis.model_name = voice_name; | ||
| event_data.data.tts_synthesis.framework = framework; | ||
| event_data.data.tts_synthesis.error_code = result; | ||
| event_data.data.tts_synthesis.error_message = "No voice loaded"; | ||
| rac_analytics_event_emit(RAC_EVENT_TTS_SYNTHESIS_FAILED, &event_data); | ||
| return result; | ||
| } | ||
| } |
There was a problem hiding this comment.
Same dangling pointer issue in synthesize_stream.
Apply the same fix here: copy voice_id and voice_name to std::string local variables before releasing the lock.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/features/tts/tts_component.cpp` around lines 448
- 477, In synthesize_stream, avoid returning/using dangling const char* from
rac_lifecycle_get_model_id/name after releasing component->mtx: inside the
locked region (where voice_id/voice_name are assigned and local_options is
copied) immediately copy those values into std::string local variables (e.g.
voice_id_str, voice_name_str) and then release the lock; later use those
std::string.c_str() (or the std::string values) when filling event_data and
logging (references: synthesize_stream, voice_id, voice_name,
rac_lifecycle_get_model_id, rac_lifecycle_get_model_name, component->mtx,
local_options). Ensure all places that currently reference voice_id/voice_name
after the lock are updated to use the safe std::string locals.
| { | ||
| std::lock_guard<std::mutex> lock(handle->mutex); | ||
| for (auto& pair : handle->entries) { free_lora_entry(pair.second); } | ||
| handle->entries.clear(); | ||
| } | ||
| delete handle; |
There was a problem hiding this comment.
Destroy is still unsafe under concurrent access.
This lock only serializes entries cleanup. A thread already waiting in another API on handle->mutex can continue after Line 104 and dereference a freed handle, so the change still leaves a use-after-free window. Please either make rac_lora_registry_destroy an externally synchronized operation and document that contract, or add explicit lifetime management before deleting the registry.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-commons/src/infrastructure/model_management/lora_registry.cpp`
around lines 99 - 104, The destroy path in rac_lora_registry_destroy is unsafe
because it deletes the handle while other APIs may still be blocked on or about
to use handle->mutex; fix by adding explicit lifetime management: add an
atomic<bool> destroying (or state) and an atomic<int> active_ops (or ref_count)
to the registry handle, update every public API that uses the handle (the
functions that lock handle->mutex and access handle->entries) to increment
active_ops at entry and decrement at exit (and check destroying to fail early),
and change rac_lora_registry_destroy to set destroying=true, wait (using a
condition_variable or busy-wait on active_ops==0) for active_ops to reach zero
while holding the mutex, then perform the entries cleanup (free_lora_entry on
each) and delete handle; alternatively, if you prefer the simpler contract,
document in rac_lora_registry_destroy's API that the caller must externally
synchronize all concurrent uses of the registry and do not change deletion
logic.
| final archivePathPtr = archivePath.toNativeUtf8(); | ||
| final destPathPtr = destinationPath.toNativeUtf8(); | ||
|
|
||
| for (var i = 0; i < archive.files.length; i++) { | ||
| final file = archive.files[i]; | ||
| final filename = file.name; | ||
| try { | ||
| final result = extractFn( | ||
| archivePathPtr, | ||
| destPathPtr, | ||
| nullptr, // default options | ||
| nullptr, // no progress callback | ||
| nullptr, // no user data | ||
| nullptr, // no result output | ||
| ); | ||
|
|
||
| if (file.isFile) { | ||
| final outputFile = File('$destinationPath/$filename'); | ||
| await outputFile.parent.create(recursive: true); | ||
| await outputFile.writeAsBytes(file.content as List<int>); | ||
| if (result != 0) { | ||
| throw SDKError.downloadFailed( | ||
| archivePath, | ||
| 'Native extraction failed with code: $result', | ||
| ); | ||
| } | ||
|
|
||
| onProgress?.call((i + 1) / totalFiles); | ||
| } finally { | ||
| calloc.free(archivePathPtr); | ||
| calloc.free(destPathPtr); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Dart FFI toNativeUtf8 default allocator
💡 Result:
String.toNativeUtf8() (from package:ffi/ffi.dart) uses malloc as its default allocator:
Pointer<Utf8> toNativeUtf8({Allocator allocator = malloc})So if you don’t pass an allocator:, the returned pointer must be released with the same allocator, typically:
final p = 'hello'.toNativeUtf8();
malloc.free(p);[1][2]
Memory allocator mismatch: allocated with malloc, freed with calloc.
toNativeUtf8() uses the malloc allocator by default, but the code frees the pointers using calloc.free(). This mismatch causes undefined behavior and can lead to memory corruption or crashes. Use malloc.free() to match the allocation method.
🐛 Proposed fix
final archivePathPtr = archivePath.toNativeUtf8();
final destPathPtr = destinationPath.toNativeUtf8();
try {
final result = extractFn(
archivePathPtr,
destPathPtr,
nullptr, // default options
nullptr, // no progress callback
nullptr, // no user data
nullptr, // no result output
);
if (result != 0) {
throw SDKError.downloadFailed(
archivePath,
'Native extraction failed with code: $result',
);
}
} finally {
- calloc.free(archivePathPtr);
- calloc.free(destPathPtr);
+ malloc.free(archivePathPtr);
+ malloc.free(destPathPtr);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final archivePathPtr = archivePath.toNativeUtf8(); | |
| final destPathPtr = destinationPath.toNativeUtf8(); | |
| for (var i = 0; i < archive.files.length; i++) { | |
| final file = archive.files[i]; | |
| final filename = file.name; | |
| try { | |
| final result = extractFn( | |
| archivePathPtr, | |
| destPathPtr, | |
| nullptr, // default options | |
| nullptr, // no progress callback | |
| nullptr, // no user data | |
| nullptr, // no result output | |
| ); | |
| if (file.isFile) { | |
| final outputFile = File('$destinationPath/$filename'); | |
| await outputFile.parent.create(recursive: true); | |
| await outputFile.writeAsBytes(file.content as List<int>); | |
| if (result != 0) { | |
| throw SDKError.downloadFailed( | |
| archivePath, | |
| 'Native extraction failed with code: $result', | |
| ); | |
| } | |
| onProgress?.call((i + 1) / totalFiles); | |
| } finally { | |
| calloc.free(archivePathPtr); | |
| calloc.free(destPathPtr); | |
| } | |
| final archivePathPtr = archivePath.toNativeUtf8(); | |
| final destPathPtr = destinationPath.toNativeUtf8(); | |
| try { | |
| final result = extractFn( | |
| archivePathPtr, | |
| destPathPtr, | |
| nullptr, // default options | |
| nullptr, // no progress callback | |
| nullptr, // no user data | |
| nullptr, // no result output | |
| ); | |
| if (result != 0) { | |
| throw SDKError.downloadFailed( | |
| archivePath, | |
| 'Native extraction failed with code: $result', | |
| ); | |
| } | |
| } finally { | |
| malloc.free(archivePathPtr); | |
| malloc.free(destPathPtr); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere_onnx/lib/onnx_download_strategy.dart`
around lines 229 - 251, The pointers returned by toNativeUtf8() (archivePathPtr
and destPathPtr in the extraction block) are allocated with malloc but are being
freed with calloc.free(), causing an allocator mismatch; update the finally
block to free those pointers with malloc.free() (use malloc.free(archivePathPtr)
and malloc.free(destPathPtr)) so allocation and deallocation use the same
allocator and avoid undefined behavior in the extractFn call and surrounding
code.
There was a problem hiding this comment.
Actionable comments posted: 20
♻️ Duplicate comments (3)
sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp (1)
74-80:⚠️ Potential issue | 🟠 MajorDifferentiate OOM from constructor failures.
Line 75’s
new (std::nothrow)only returnsnullptron allocation failure. Ifrac_vad_analytics_s()throws during construction, thecatch (...)path here still reportsRAC_ERROR_OUT_OF_MEMORY, which gives callers the wrong failure reason and drops the exception details.Suggested fix
- try { - *out_handle = new (std::nothrow) rac_vad_analytics_s(); - } catch (...) { - *out_handle = nullptr; - } - if (!*out_handle) { - return RAC_ERROR_OUT_OF_MEMORY; - } + try { + *out_handle = new (std::nothrow) rac_vad_analytics_s(); + } catch (const std::exception& ex) { + *out_handle = nullptr; + log_error("VAD.Analytics", "Failed to create VAD analytics service: %s", ex.what()); + return RAC_ERROR_INTERNAL; + } catch (...) { + *out_handle = nullptr; + log_error("VAD.Analytics", "Failed to create VAD analytics service: unknown exception"); + return RAC_ERROR_INTERNAL; + } + if (!*out_handle) { + return RAC_ERROR_OUT_OF_MEMORY; + }In C++, does `new (std::nothrow) T(...)` return `nullptr` when `T`'s constructor throws, or does it only return `nullptr` for allocation failure while constructor exceptions still propagate?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp` around lines 74 - 80, The current try/catch masks constructor exceptions as RAC_ERROR_OUT_OF_MEMORY; change the logic around rac_vad_analytics_s creation so allocation null-check and constructor exceptions are handled separately: use new (std::nothrow) to attempt allocation, check if the returned pointer is nullptr and return RAC_ERROR_OUT_OF_MEMORY, and separately catch constructor exceptions (catch std::exception& to capture e.what() or a catch(...) fallback) to return a distinct construction failure code (or propagate/log the exception) and set *out_handle = nullptr; refer to symbols rac_vad_analytics_s, out_handle, and RAC_ERROR_OUT_OF_MEMORY to locate and update the block.sdk/runanywhere-commons/src/server/http_server.cpp (1)
87-113:⚠️ Potential issue | 🟠 MajorClean up partially initialized state if
start()throws.The new
rac_server_start()catch block changes the return code, but it does not undo the mutations already made here. IfloadModel()succeeds and a later operation on Line 94 or Line 112 throws, the singleton keeps a livellmHandle_/server_and the next start runs against half-initialized state.Proposed fix
rac_result_t HttpServer::start(const rac_server_config_t& config) { static constexpr int SERVER_START_POLL_ITERATIONS = 100; static constexpr int SERVER_START_POLL_MS = 100; { std::lock_guard<std::mutex> lock(mutex_); + try { if (running_) { return RAC_ERROR_SERVER_ALREADY_RUNNING; } // ... validation and state copy ... rac_result_t rc = loadModel(modelPath_); if (RAC_FAILED(rc)) { return rc; } server_ = std::make_unique<httplib::Server>(); if (config.enable_cors == RAC_TRUE) { setupCors(); } setupRoutes(); shouldStop_ = false; activeRequests_ = 0; totalRequests_ = 0; totalTokensGenerated_ = 0; startTime_ = std::chrono::steady_clock::now(); serverThread_ = std::thread(&HttpServer::serverThread, this); + } catch (...) { + shouldStop_ = true; + if (serverThread_.joinable()) { + serverThread_.join(); + } + unloadModel(); + server_.reset(); + throw; + } }#!/bin/bash # Verify that HttpServer::start performs throwing work after loadModel() # and that cleanup is not handled inside the method. sed -n '81,140p' sdk/runanywhere-commons/src/server/http_server.cpp printf '\n--- throwing operations in start() ---\n' rg -n "loadModel|std::make_unique|std::thread|setupRoutes|setupCors|catch" \ sdk/runanywhere-commons/src/server/http_server.cpp -C 2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/server/http_server.cpp` around lines 87 - 113, The start() method can leave partially initialized members (llmHandle_, server_, serverThread_, and state flags) set if a later operation throws after loadModel() succeeds; update HttpServer::start to wrap the initialization steps (loadModel, server_ = std::make_unique, setupCors, setupRoutes, starting serverThread_) in a try/catch (or use RAII) and on any exception explicitly undo/cleanup: unload or reset llmHandle_ (inverse of loadModel), reset server_ to nullptr, stop/join and reset serverThread_ if it was started, and restore shouldStop_, activeRequests_, totalRequests_, totalTokensGenerated_ and startTime_ to safe defaults before rethrowing or returning the error code so the singleton cannot be left half-initialized.sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp (1)
37-65:⚠️ Potential issue | 🔴 CriticalUse one normalized safety check for entry paths and link targets.
is_path_safe()still allows a single leading backslash (\foo), and the symlink branch on Lines 236-245 bypasses the helper entirely. A crafted archive can still plant rooted Windows link targets even though regular entry paths are screened.Also applies to: 236-245
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp` around lines 37 - 65, is_path_safe currently misses single leading backslashes and the symlink extraction branch bypasses it, allowing rooted Windows link targets; update is_path_safe to reject any path that begins with either '/' or '\\' (not just UNC), keep the existing drive-letter and UNC checks, and ensure the ".." component check treats both separators consistently; then call this same normalized is_path_safe for both archive entry names and symlink targets (replace the ad-hoc symlink handling in the symlink extraction branch with a call to is_path_safe) so all targets use a single, unified validation routine.
🧹 Nitpick comments (6)
sdk/runanywhere-commons/src/features/vad/vad_component.cpp (1)
195-198: Consider consistent memory ordering for atomic operations.The code mixes explicit
memory_order_acquireloads (lines 185, 482) with implicit atomic operations that usememory_order_seq_cst(lines 195, 231, 251, 317, 384, 498). Whileseq_cstis correct, using explicit ordering throughout would be clearer:
- Stores at lines 231 and 251 should use
.store(value, std::memory_order_release)to pair with the acquire loads.- Reads at lines 195, 317, 384, 498 could use
.load(std::memory_order_acquire)for consistency.Proposed fix for consistent memory ordering
// In rac_vad_component_initialize (line 195): - if (component->is_initialized) { + if (component->is_initialized.load(std::memory_order_acquire)) { // In rac_vad_component_initialize (line 231): - component->is_initialized = true; + component->is_initialized.store(true, std::memory_order_release); // In rac_vad_component_cleanup (line 251): - component->is_initialized = false; + component->is_initialized.store(false, std::memory_order_release);Also applies to: 231-231, 251-251
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp` around lines 195 - 198, The code mixes explicit acquire loads with default seq_cst atomics; update the atomic operations for consistency: change the read of component->is_initialized to use .load(std::memory_order_acquire) and change the corresponding state stores (the .store(...) calls that set the initialized/running flags in the VAD component) to use .store(value, std::memory_order_release) so they pair with the acquire loads; apply the same change to other atomic reads/ writes referenced in this file (the atomic state flags used in the VAD init/start/stop logic) to use explicit memory_order_acquire for loads and memory_order_release for stores.sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp (1)
242-247: Consider marking all subscriptions as dead before clearing.If
reset_event_publisher()is ever called while a publish is in flight (even in tests), the alive flags in snapshotted subscriptions won't be updated. Consider iterating and settingalive->store(false)before clearing the containers for consistency with the unsubscribe pattern.♻️ Suggested change
void reset_event_publisher() { std::lock_guard<std::mutex> lock(g_event_mutex); + // Mark all subscriptions as dead before clearing + for (auto& [_, subs] : g_subscriptions) { + for (auto& s : subs) { + s.alive->store(false); + } + } + for (auto& s : g_all_subscriptions) { + s.alive->store(false); + } g_subscriptions.clear(); g_all_subscriptions.clear(); g_next_subscription_id.store(1); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp` around lines 242 - 247, In reset_event_publisher(), before clearing g_subscriptions and g_all_subscriptions, iterate the stored subscription entries (e.g., the entries in g_all_subscriptions and/or g_subscriptions) and set each subscription's alive flag to false (call alive->store(false)) while holding g_event_mutex so any in-flight publish snapshots see the subscriptions as dead; then clear the containers and reset g_next_subscription_id as before to mirror the unsubscribe pattern.sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart (1)
377-382: Replace magic number with a named constant.The literal
99forRAC_ARCHIVE_STRUCTURE_UNKNOWNworks correctly but reduces maintainability. If the C++ enum value changes, this code will silently break.Consider adding this constant to
model_types_cpp_bridge.dartalongsideRacModelFormatandRacInferenceFramework:Suggested constant definition (in model_types_cpp_bridge.dart)
/// Archive structure constants (rac_archive_structure_t) abstract class RacArchiveStructure { static const int singleFileNested = 0; static const int directoryBased = 1; static const int nestedDirectory = 2; static const int unknown = 99; }Then update this call:
final modelPath = DartBridgeDownload.findModelPathAfterExtraction( extractedDir: destDir, - structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect + structure: RacArchiveStructure.unknown, framework: framework.toC(), format: format.toC(), );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart` around lines 377 - 382, Replace the magic number 99 passed to DartBridgeDownload.findModelPathAfterExtraction with a named constant; add an abstract RacArchiveStructure class in model_types_cpp_bridge.dart containing the constants (singleFileNested, directoryBased, nestedDirectory, unknown) and then call findModelPathAfterExtraction with RacArchiveStructure.unknown instead of 99 so the intent is clear and the value is centralized (refer to DartBridgeDownload.findModelPathAfterExtraction and model_types_cpp_bridge.dart / RacArchiveStructure).sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp (1)
458-474: Sharedcancel_requestedflag can affect concurrent requests.The
cancel_requestedflag is shared across all generate calls. If concurrent generations are allowed (the lock is released before generation), callingcancel()will abort all in-flight generations, not just one.If this is intentional (component-level cancellation), document it. If per-request cancellation is desired, consider adding a request-local cancel token.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp` around lines 458 - 474, The progress wrapper currently checks the shared component-level flag ctx->component->cancel_requested in diffusion_progress_wrapper, which will abort all concurrent generations; either document that cancellation is component-wide or make it per-request by adding a request-local cancel token inside diffusion_callback_context (e.g., request_cancelled or an atomic/rac_bool_t) and have diffusion::cancel() set that token for the specific request instead of flipping component->cancel_requested; update diffusion_callback_context, cancel(), and diffusion_progress_wrapper to reference the new per-request token (or add clear comments explaining component-level behavior) so cancellations only affect the intended generation.sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp (1)
393-407:require_servicedoesn't pin the service—consider documenting or deprecating.Unlike the new
acquire_service,require_servicereturns the service handle without incrementing the refcount. This means the service can be unloaded while the caller is using it.Consider:
- Documenting that
require_serviceis only safe when the caller holds an external lock that prevents unload, OR- Deprecating
require_servicein favor ofacquire_service🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp` around lines 393 - 407, The function rac_lifecycle_require_service returns a raw service handle without incrementing its refcount (see rac_lifecycle_require_service, LifecycleManager::current_service and state), which lets the service be unloaded while in use; either document this safety requirement or deprecate the API: mark rac_lifecycle_require_service as deprecated and update callers to use the new acquire_service that pins the service (or change rac_lifecycle_require_service to forward to acquire_service and return a pinned handle), and add a clear comment on the function explaining that callers must hold an external lock if they continue to use the unpinned handle.sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h (1)
237-254: Documentation should clarify edge cases and thread-safety.The new acquire/release API is well-documented for the happy path, but consider adding:
- Underflow behavior: What happens if
release_serviceis called without a prioracquire_service(refcount underflow)?- Thread-safety: Explicitly state that these functions are thread-safe.
- Pairing requirement: Emphasize that each
acquire_servicecall MUST be paired with exactly onerelease_servicecall.As per coding guidelines, public C API headers must document error codes and lifecycle requirements.
📝 Suggested documentation additions
/** * `@brief` Acquire (pin) the current service, preventing unload while held. * * Increments an internal refcount. The caller MUST call rac_lifecycle_release_service() * when done. Unload/destroy will block until all acquired references are released. + * + * Thread-safe: May be called concurrently from multiple threads. + * + * `@note` Each successful acquire MUST be paired with exactly one release. + * Failure to release will cause unload/destroy to block indefinitely. * * `@param` handle Lifecycle manager handle * `@param` out_service Output: Service handle (pinned) * `@return` RAC_SUCCESS or RAC_ERROR_NOT_INITIALIZED if not loaded */ /** * `@brief` Release a previously acquired service reference. + * + * Thread-safe: May be called concurrently from multiple threads. + * + * `@warning` Calling release without a prior acquire results in undefined behavior + * (refcount underflow). * * `@param` handle Lifecycle manager handle */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h` around lines 237 - 254, Update the documentation for rac_lifecycle_acquire_service and rac_lifecycle_release_service to explicitly state thread-safety, the required 1:1 pairing (each rac_lifecycle_acquire_service must be matched by exactly one rac_lifecycle_release_service), the behavior on refcount underflow (define whether release without acquire is a no-op, asserts, or returns/logs an error), and enumerate possible return/error codes (e.g., RAC_SUCCESS, RAC_ERROR_NOT_INITIALIZED, and any error for invalid release), so callers know lifecycle requirements and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@sdk/runanywhere-commons/CMakeLists.txt`:
- Around line 177-180: The build currently disables LZMA (set ENABLE_LZMA OFF)
while the public API and detector still advertise and detect
RAC_ARCHIVE_TYPE_TAR_XZ, causing rac_detect_archive_type() to report .tar.xz but
rac_extract_archive_native() to fail at runtime; either enable LZMA by turning
ENABLE_LZMA ON in CMakeLists so libarchive has the xz decompressor, or
remove/guard RAC_ARCHIVE_TYPE_TAR_XZ from the public header and the detection
logic in rac_extraction.cpp (and any references in rac_detect_archive_type() and
rac_extract_archive_native()) so callers can no longer detect or select tar.xz
when the decompressor is unavailable.
In
`@sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h`:
- Around line 90-143: The header is missing lifecycle documentation for the
progress callback and input/output pointers; update the comments for
rac_extract_archive_native and rac_extraction_progress_fn to state that the
progress callback (rac_extraction_progress_fn) is invoked synchronously on the
calling thread during extraction, that user_data and the options pointer
(rac_extraction_options_t*) must remain valid and unchanged for the entire
duration of the rac_extract_archive_native call (and until the function returns)
because no background threads are used, and that out_result, if non-NULL, will
be populated only on successful completion (RAC_SUCCESS) and may be left
unmodified or only partially filled on failure—document the exact failure
semantics (e.g., zero/unchanged) so callers know how to handle it; also add a
brief note on rac_detect_archive_type that file_path and out_type must remain
valid only for the call and are not retained.
In `@sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp`:
- Around line 1208-1211: configure_vad currently only updates config_ but does
not apply those settings to the live detector (sherpa_vad_), so process()
continues using old thresholds; modify configure_vad to acquire mutex_, set
config_, and then apply the new VAD parameters to the running detector — either
by calling the appropriate setter(s) on sherpa_vad_ or by
reinitializing/reloading it similarly to load_model() so threshold/window are
applied; ensure get_vad_config() remains consistent with the active detector and
protect changes with mutex_ (refer to configure_vad, load_model, process,
get_vad_config, and the sherpa_vad_ member).
- Around line 966-975: The code currently destroys/replaces sherpa_tts_ while
syntheses may be in progress, causing a dangling tts_ptr; modify the
reload/unload logic (where sherpa_tts_, new_tts, SherpaOnnxDestroyOfflineTts and
unload_model() are used) to either wait until active_synthesis_count_ == 0
before calling SherpaOnnxDestroyOfflineTts and assigning sherpa_tts_ = new_tts,
or convert sherpa_tts_ to a shared/ref-counted handle (e.g., shared_ptr-like
wrapper) so in-progress synthesis holds a reference and destruction waits for
last user; ensure the mutex still protects pointer swap and that
active_synthesis_count_ is incremented/decremented around synthesis start/finish
to coordinate the safe destroy/swap.
In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp`:
- Around line 267-270: The current blocking wait on mgr->service_cv using
mgr->service_refcount can hang forever if a release is missed; update the
unload()/reset() logic that uses mgr->service_cv.wait(lock, [mgr]{...}) to use a
timed wait (e.g., wait_for with a reasonable timeout) and after each timeout
check log a warning containing the current service_refcount and continue waiting
or break after a maximum number of retries; reference the same synchronization
objects (mgr->service_cv, mgr->mutex, mgr->service_refcount) and ensure the
warning log is emitted when a timeout occurs to aid debugging hung shutdowns.
- Around line 249-259: The refcount underflow in rac_lifecycle_release_service
must be prevented: modify rac_lifecycle_release_service to atomically decrement
service_refcount only if its current value is greater than zero (use an atomic
compare-exchange loop on LifecycleManager::service_refcount or load-and-compare
with compare_exchange_weak) and return immediately if it is already zero; when a
successful decrement yields previous==1 then call mgr->service_cv.notify_all(),
and optionally log or assert on an attempted release when count was zero to
surface misuse.
In `@sdk/runanywhere-commons/src/features/voice_agent/voice_agent.cpp`:
- Around line 781-804: The callback typedef rac_voice_agent_event_callback_fn
lacks required lifetime documentation: update the rac_voice_agent.h declaration
for rac_voice_agent_event_callback_fn (and the rac_voice_agent_event_t field
comments if present) to state explicitly that all pointers inside the event
(e.g., transcription, response, synthesized_audio) are owned by the caller and
valid only for the duration of the callback invocation and must not be retained
or freed by the callee; mention that the implementation will free those buffers
immediately after the callback returns so callers must copy any data they need
to keep.
In
`@sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp`:
- Around line 459-476: The temp archive path generation in the needs_extraction
branch (where download_dest is constructed using downloads_dir + "/" + stem +
"." + ext, relying on get_file_extension and get_filename_stem and model_id) is
not task-unique and can collide across concurrent tasks; change this to produce
a unique per-task temp filename (e.g., incorporate a generated UUID, the task
ID, or use a secure temp-name API like mkstemp/mkstemp-like behavior) when
creating download_dest so concurrent downloads don't overwrite each other's temp
archives; apply the same change to the other occurrence around the 771-783 logic
that constructs a temp archive path.
- Around line 595-615: The loop constructing file_url and dest_path must
validate and sanitize each rac_model_file_descriptor_t before using
file.relative_path or file.destination_path: first check for nullptr and
non-empty strings before converting to std::string (avoid UB) and reject or skip
descriptors where both are missing; normalize backslashes to '/', reject
absolute paths (leading '/' or Windows roots like 'C:' or UNC '\\') and any
segment equal to ".." to prevent directory traversal, and only then join with
model_folder/base_download_url; also ensure mkdir_p() receives the sanitized
parent directory. Use the existing loop and local variables (file,
file.relative_path, file.destination_path, file_url, dest_path, model_folder,
base_download_url, mkdir_p) to locate where to add these checks and to log/skip
invalid descriptors.
In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp`:
- Around line 143-145: The code copies rac_extraction_options_t opts but ignores
opts.archive_type_hint when initializing the reader (the reader is always set to
auto-detect); update the reader initialization logic in rac_extraction.cpp so
that if opts.archive_type_hint is set (not the sentinel/unknown value) you
configure the reader to use that explicit archive type instead of full
auto-detection—use the existing reader-config API you call on lines 164-166 but
branch on opts.archive_type_hint and pass the forced type to the reader
initialization (preserving auto-detect when archive_type_hint is unset).
- Around line 347-385: The function rac_detect_archive_type leaves the caller's
out_type unchanged on failure paths; update rac_detect_archive_type to set
*out_type to a safe default (e.g., RAC_ARCHIVE_TYPE_UNKNOWN or a defined
RAC_ARCHIVE_TYPE_NONE) before any early return (including nullptr checks, fopen
failure, fread short read, and final no-match return) so callers never see stale
enum values from previous calls; reference the out parameter out_type and the
function rac_detect_archive_type when making this change.
- Around line 308-327: The code currently ignores return values from
archive_write_finish_entry(ext) and archive_write_free(ext); update
rac_extraction.cpp to check both returns and set status =
RAC_ERROR_EXTRACTION_FAILED (and perform the same cleanup/logging used for other
I/O errors) when they return != ARCHIVE_OK instead of proceeding to report
success. Specifically, after calling archive_write_finish_entry(ext) in the
per-entry loop, test its return and handle failure exactly like the existing
data I/O error path (set status, break/cleanup); and before final return, call
archive_write_free(ext), check its return, and if not ARCHIVE_OK set status =
RAC_ERROR_EXTRACTION_FAILED so the function doesn’t return RAC_SUCCESS on silent
write/close failures.
In `@sdk/runanywhere-commons/src/server/http_server.cpp`:
- Around line 226-230: The code currently invokes callbacks (requestCallback_,
errorCallback_) while holding callback_mutex_, which can deadlock if those
callbacks call rac_server_set_request_callback() or
rac_server_set_error_callback() and also serializes request handling; fix by
under callback_mutex_ copying the function pointers and user-data (e.g., copy
requestCallback_, requestCallbackUserData_, errorCallback_,
errorCallbackUserData_ into local variables) and then release the lock and
invoke the copied callbacks (for the GET "/v1/models" path, the POST path, and
the error path) outside the lock so the mutex is not held during external
callback execution.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`:
- Around line 364-367: The throw uses an undefined DownloadException causing a
compile error; change the thrown type to the standard Exception and keep the
same message. Update the native extraction failure block (where _logger.error
logs 'Native extraction failed with code: $result' and the subsequent throw) to
throw Exception('Native extraction failed with code: $result') instead of
DownloadException; this aligns with other throws in this file that use
Exception.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`:
- Around line 265-268: The checkStorage method currently treats a null response
from checkStorageAvailability as “available” which lets downloads proceed when
the bridge or native symbol is missing; change checkStorage to fail closed by
returning false when checkStorageAvailability returns null or throws: call
checkStorageAvailability(requiredBytes), if the result is null return false,
otherwise return result.isAvailable; optionally catch and log any exceptions
from checkStorageAvailability and return false to ensure downloads are blocked
on lookup/registration failures (refer to checkStorage and
checkStorageAvailability).
- Around line 398-410: The loop allocates a contiguous buffer (entries via
calloc<Pointer<Utf8>>(count)) and individual Utf8 strings (name.toNativeUtf8())
but on exception the catch returns without freeing any of those, leaking native
memory; update the error path so that if an exception occurs during the for-loop
you iterate over the already-allocated indices (0..i-1) and free each entries[j]
(the pointers returned by toNativeUtf8()) and then free the entries buffer
itself (calloc.free(entries) or the appropriate allocator free), before setting
outEntries.value = nullptr, outCount.value = 0 and returning _errorFileNotFound;
apply this cleanup inside the existing try/catch around the loop that references
contents, entries, toNativeUtf8, outEntries, outCount and _errorFileNotFound.
- Around line 102-104: The native call in dart_bridge_file_manager.dart
currently ignores the native return code (calling fn(_callbacksPtr!, pathPtr,
sizePtr)) and always returns sizePtr.value, which collapses failures into a
zero-size result; update the call sites (including the method shown and the
similar implementations in modelsStorageUsed() and cacheSize()) to check the
native function's return value, handle non-success results by throwing or
returning an error/nullable result instead of returning sizePtr.value, and only
read and return sizePtr.value when the native return indicates success (use the
exact symbols _callbacksPtr, fn, pathPtr, sizePtr, modelsStorageUsed, cacheSize
to find the spots).
In
`@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`:
- Around line 129-140: The code in the readdir loop (using entry, strdup and
entries) doesn't check the result of strdup, so a failed allocation yields a
null entry and the function still returns RAC_SUCCESS; update the loop in
FileManagerBridge.cpp to check the return of strdup(entry->d_name) each
iteration, and on failure free any previously strdup'd strings and the entries
array, closedir(dir), set *outEntries and *outCount appropriately (e.g.,
null/0), and return a failure code (not RAC_SUCCESS) so callers don't receive a
partially populated entries array; ensure you reference entries, entry, strdup,
closedir, outEntries, outCount and replace the final RAC_SUCCESS return with an
error path when strdup fails.
- Around line 53-56: The current mkdir handling in FileManagerBridge.cpp returns
RAC_SUCCESS whenever mkdir() succeeds or errno == EEXIST, which incorrectly
treats existing files/symlinks as a created directory; update the logic so that
when mkdir fails with errno == EEXIST you call stat/lstat on path and verify
S_ISDIR(st_mode) before returning RAC_SUCCESS, otherwise return
RAC_ERROR_FILE_IO; keep the existing return values (RAC_SUCCESS /
RAC_ERROR_FILE_IO) and use the same variables (path, errno) and functions
(mkdir) so the change is limited to adding the directory check.
- Around line 172-194: The space callbacks posixGetAvailableSpace and
posixGetTotalSpace currently return 0 on statvfs() failure which violates the
callback contract (should return -1 on error) and can be mistaken for valid
space; update both functions so that when statvfs(mountPoint, &vfs) != 0 they
return a signed error sentinel (e.g., static_cast<int64_t>(-1)) instead of 0,
preserving the existing logic and types for the successful return path.
---
Duplicate comments:
In `@sdk/runanywhere-commons/src/features/vad/vad_analytics.cpp`:
- Around line 74-80: The current try/catch masks constructor exceptions as
RAC_ERROR_OUT_OF_MEMORY; change the logic around rac_vad_analytics_s creation so
allocation null-check and constructor exceptions are handled separately: use new
(std::nothrow) to attempt allocation, check if the returned pointer is nullptr
and return RAC_ERROR_OUT_OF_MEMORY, and separately catch constructor exceptions
(catch std::exception& to capture e.what() or a catch(...) fallback) to return a
distinct construction failure code (or propagate/log the exception) and set
*out_handle = nullptr; refer to symbols rac_vad_analytics_s, out_handle, and
RAC_ERROR_OUT_OF_MEMORY to locate and update the block.
In `@sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp`:
- Around line 37-65: is_path_safe currently misses single leading backslashes
and the symlink extraction branch bypasses it, allowing rooted Windows link
targets; update is_path_safe to reject any path that begins with either '/' or
'\\' (not just UNC), keep the existing drive-letter and UNC checks, and ensure
the ".." component check treats both separators consistently; then call this
same normalized is_path_safe for both archive entry names and symlink targets
(replace the ad-hoc symlink handling in the symlink extraction branch with a
call to is_path_safe) so all targets use a single, unified validation routine.
In `@sdk/runanywhere-commons/src/server/http_server.cpp`:
- Around line 87-113: The start() method can leave partially initialized members
(llmHandle_, server_, serverThread_, and state flags) set if a later operation
throws after loadModel() succeeds; update HttpServer::start to wrap the
initialization steps (loadModel, server_ = std::make_unique, setupCors,
setupRoutes, starting serverThread_) in a try/catch (or use RAII) and on any
exception explicitly undo/cleanup: unload or reset llmHandle_ (inverse of
loadModel), reset server_ to nullptr, stop/join and reset serverThread_ if it
was started, and restore shouldStop_, activeRequests_, totalRequests_,
totalTokensGenerated_ and startTime_ to safe defaults before rethrowing or
returning the error code so the singleton cannot be left half-initialized.
---
Nitpick comments:
In `@sdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.h`:
- Around line 237-254: Update the documentation for
rac_lifecycle_acquire_service and rac_lifecycle_release_service to explicitly
state thread-safety, the required 1:1 pairing (each
rac_lifecycle_acquire_service must be matched by exactly one
rac_lifecycle_release_service), the behavior on refcount underflow (define
whether release without acquire is a no-op, asserts, or returns/logs an error),
and enumerate possible return/error codes (e.g., RAC_SUCCESS,
RAC_ERROR_NOT_INITIALIZED, and any error for invalid release), so callers know
lifecycle requirements and error handling.
In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp`:
- Around line 393-407: The function rac_lifecycle_require_service returns a raw
service handle without incrementing its refcount (see
rac_lifecycle_require_service, LifecycleManager::current_service and state),
which lets the service be unloaded while in use; either document this safety
requirement or deprecate the API: mark rac_lifecycle_require_service as
deprecated and update callers to use the new acquire_service that pins the
service (or change rac_lifecycle_require_service to forward to acquire_service
and return a pinned handle), and add a clear comment on the function explaining
that callers must hold an external lock if they continue to use the unpinned
handle.
In `@sdk/runanywhere-commons/src/features/diffusion/diffusion_component.cpp`:
- Around line 458-474: The progress wrapper currently checks the shared
component-level flag ctx->component->cancel_requested in
diffusion_progress_wrapper, which will abort all concurrent generations; either
document that cancellation is component-wide or make it per-request by adding a
request-local cancel token inside diffusion_callback_context (e.g.,
request_cancelled or an atomic/rac_bool_t) and have diffusion::cancel() set that
token for the specific request instead of flipping component->cancel_requested;
update diffusion_callback_context, cancel(), and diffusion_progress_wrapper to
reference the new per-request token (or add clear comments explaining
component-level behavior) so cancellations only affect the intended generation.
In `@sdk/runanywhere-commons/src/features/vad/vad_component.cpp`:
- Around line 195-198: The code mixes explicit acquire loads with default
seq_cst atomics; update the atomic operations for consistency: change the read
of component->is_initialized to use .load(std::memory_order_acquire) and change
the corresponding state stores (the .store(...) calls that set the
initialized/running flags in the VAD component) to use .store(value,
std::memory_order_release) so they pair with the acquire loads; apply the same
change to other atomic reads/ writes referenced in this file (the atomic state
flags used in the VAD init/start/stop logic) to use explicit
memory_order_acquire for loads and memory_order_release for stores.
In `@sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp`:
- Around line 242-247: In reset_event_publisher(), before clearing
g_subscriptions and g_all_subscriptions, iterate the stored subscription entries
(e.g., the entries in g_all_subscriptions and/or g_subscriptions) and set each
subscription's alive flag to false (call alive->store(false)) while holding
g_event_mutex so any in-flight publish snapshots see the subscriptions as dead;
then clear the containers and reset g_next_subscription_id as before to mirror
the unsubscribe pattern.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`:
- Around line 377-382: Replace the magic number 99 passed to
DartBridgeDownload.findModelPathAfterExtraction with a named constant; add an
abstract RacArchiveStructure class in model_types_cpp_bridge.dart containing the
constants (singleFileNested, directoryBased, nestedDirectory, unknown) and then
call findModelPathAfterExtraction with RacArchiveStructure.unknown instead of 99
so the intent is clear and the value is centralized (refer to
DartBridgeDownload.findModelPathAfterExtraction and model_types_cpp_bridge.dart
/ RacArchiveStructure).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fe26c885-f15f-4e24-b3b9-b59440bf7591
📒 Files selected for processing (18)
sdk/runanywhere-commons/CMakeLists.txtsdk/runanywhere-commons/include/rac/core/capabilities/rac_lifecycle.hsdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.hsdk/runanywhere-commons/src/backends/onnx/onnx_backend.cppsdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cppsdk/runanywhere-commons/src/features/diffusion/diffusion_component.cppsdk/runanywhere-commons/src/features/llm/llm_component.cppsdk/runanywhere-commons/src/features/vad/vad_analytics.cppsdk/runanywhere-commons/src/features/vad/vad_component.cppsdk/runanywhere-commons/src/features/voice_agent/voice_agent.cppsdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cppsdk/runanywhere-commons/src/infrastructure/events/event_publisher.cppsdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cppsdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cppsdk/runanywhere-commons/src/server/http_server.cppsdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dartsdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
- sdk/runanywhere-commons/src/infrastructure/model_management/model_assignment.cpp
| set(ENABLE_LZMA OFF CACHE BOOL "" FORCE) # tar.xz not currently used by any model | ||
| set(ENABLE_ZSTD OFF CACHE BOOL "" FORCE) | ||
| set(ENABLE_ZLIB ON CACHE BOOL "" FORCE) # Needed for tar.gz and zip | ||
| set(ENABLE_BZip2 ON CACHE BOOL "" FORCE) # Needed for tar.bz2 (k2-fsa models) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'ENABLE_LZMA|ENABLE_ZLIB|ENABLE_BZip2' sdk/runanywhere-commons/CMakeLists.txt
rg -n 'TAR_XZ|tar\.xz|Supports ZIP, TAR.GZ, TAR.BZ2, TAR.XZ' \
sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h \
sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp \
sdk/runanywhere-commons/include/rac/infrastructure/model_management/rac_model_types.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1005
🏁 Script executed:
sed -n '1,15p' sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 577
🏁 Script executed:
sed -n '375,385p' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 392
🏁 Script executed:
rg -n 'RAC_ARCHIVE_TYPE_TAR_XZ' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp -A 5 -B 5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 413
🏁 Script executed:
# Check if there's any conditional handling based on LZMA/XZLIB availability
rg -n 'LZMA|xz|XZLIB|lzma' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 110
🏁 Script executed:
# Find the main extraction function
rg -n 'rac_extract\|extract_archive' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Look for where archive_read is called and how it processes the detected type
rg -n 'archive_read|archive_write|archive_read_support' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp -A 3 | head -50Repository: RunanywhereAI/runanywhere-sdks
Length of output: 1632
🏁 Script executed:
# Check if the detected archive type is actually used in the extraction logic
rg -n 'out_type\|archive_type' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp -B 3 -A 3Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Find the detect_archive_type function definition and caller
ast-grep --pattern 'rac_detect_archive_type($_) {
$$$
}'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Find the main extraction function and its signature
rg -n 'rac_extract\|RAC_ERROR_EXTRACTION\|RAC_ERROR_UNSUPPORTED' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp | head -30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Look at the file structure - find function names
rg -n '^\w+.*\(' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cpp | grep -E 'detect|extract'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 266
🏁 Script executed:
sed -n '127,180p' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 2244
🏁 Script executed:
sed -n '347,400p' sdk/runanywhere-commons/src/infrastructure/extraction/rac_extraction.cppRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1286
🏁 Script executed:
# Check if rac_detect_archive_type is in public API
rg -n 'rac_detect_archive_type' sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.hRepository: RunanywhereAI/runanywhere-sdks
Length of output: 174
🏁 Script executed:
# Check all usages of rac_detect_archive_type in codebase
rg -rn 'rac_detect_archive_type' sdk/Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2252
🏁 Script executed:
rg -n 'TAR_XZ\|tar\.xz' sdk/runanywhere-commons/tests/test_extraction.cpp -B 2 -A 2Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
grep -n 'TAR_GZ\|TAR_BZ2\|ZIP\|archive' sdk/runanywhere-commons/tests/test_extraction.cpp | head -30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2091
🏁 Script executed:
rg -n 'tar\.bz2\|BZ2' sdk/runanywhere-commons/tests/test_extraction.cpp | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 56
🏁 Script executed:
# Check the CMakeLists.txt in tests to see what's being used
cat sdk/runanywhere-commons/tests/CMakeLists.txtRepository: RunanywhereAI/runanywhere-sdks
Length of output: 7786
Fix the broken TAR.XZ support contract: disable the advertised format or enable LZMA decompression.
Lines 177-180 set ENABLE_LZMA to OFF, but the public extraction API and detector still expose RAC_ARCHIVE_TYPE_TAR_XZ in the header (line 6) and detection code (lines 378-382 in rac_extraction.cpp). This creates a broken contract: callers can detect .tar.xz files via rac_detect_archive_type(), but rac_extract_archive_native() will fail at runtime because libarchive lacks the xz decompression filter. The test suite does not cover tar.xz extraction, so the failure isn't caught.
Either enable LZMA by setting ENABLE_LZMA ON, or remove RAC_ARCHIVE_TYPE_TAR_XZ from the public API and detection logic.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/CMakeLists.txt` around lines 177 - 180, The build
currently disables LZMA (set ENABLE_LZMA OFF) while the public API and detector
still advertise and detect RAC_ARCHIVE_TYPE_TAR_XZ, causing
rac_detect_archive_type() to report .tar.xz but rac_extract_archive_native() to
fail at runtime; either enable LZMA by turning ENABLE_LZMA ON in CMakeLists so
libarchive has the xz decompressor, or remove/guard RAC_ARCHIVE_TYPE_TAR_XZ from
the public header and the detection logic in rac_extraction.cpp (and any
references in rac_detect_archive_type() and rac_extract_archive_native()) so
callers can no longer detect or select tar.xz when the decompressor is
unavailable.
| /** | ||
| * @brief Progress callback for extraction. | ||
| * | ||
| * @param files_extracted Number of files extracted so far | ||
| * @param total_files Total files in archive (0 if unknown for streaming formats) | ||
| * @param bytes_extracted Bytes written to disk so far | ||
| * @param user_data User-provided context | ||
| */ | ||
| typedef void (*rac_extraction_progress_fn)(int32_t files_extracted, int32_t total_files, | ||
| int64_t bytes_extracted, void* user_data); | ||
|
|
||
| // ============================================================================= | ||
| // EXTRACTION API | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * @brief Extract an archive using native libarchive. | ||
| * | ||
| * Performs streaming extraction with constant memory usage. | ||
| * Auto-detects archive format from magic bytes if archive_type_hint | ||
| * is RAC_ARCHIVE_TYPE_NONE. | ||
| * | ||
| * @param archive_path Path to the archive file | ||
| * @param destination_dir Directory to extract into (created if needed) | ||
| * @param options Extraction options (NULL for defaults) | ||
| * @param progress_callback Progress callback (can be NULL) | ||
| * @param user_data Context for progress callback | ||
| * @param out_result Output: extraction statistics (can be NULL) | ||
| * @return RAC_SUCCESS on success, error code on failure | ||
| * | ||
| * Error codes: | ||
| * - RAC_ERROR_EXTRACTION_FAILED: General extraction error | ||
| * - RAC_ERROR_UNSUPPORTED_ARCHIVE: Unrecognized archive format | ||
| * - RAC_ERROR_FILE_NOT_FOUND: Archive file does not exist | ||
| * - RAC_ERROR_NULL_POINTER: archive_path or destination_dir is NULL | ||
| */ | ||
| RAC_API rac_result_t rac_extract_archive_native(const char* archive_path, | ||
| const char* destination_dir, | ||
| const rac_extraction_options_t* options, | ||
| rac_extraction_progress_fn progress_callback, | ||
| void* user_data, | ||
| rac_extraction_result_t* out_result); | ||
|
|
||
| /** | ||
| * @brief Detect archive type from file magic bytes. | ||
| * | ||
| * Reads the first few bytes of the file to determine the archive format. | ||
| * More reliable than file extension detection. | ||
| * | ||
| * @param file_path Path to the file | ||
| * @param out_type Output: detected archive type | ||
| * @return RAC_TRUE if archive type detected, RAC_FALSE otherwise | ||
| */ | ||
| RAC_API rac_bool_t rac_detect_archive_type(const char* file_path, rac_archive_type_t* out_type); |
There was a problem hiding this comment.
Document callback and output lifetimes in this public header.
The error codes are listed, but the lifecycle requirements are still missing: callers need to know whether progress_callback is synchronous, how long user_data / options / output buffers must stay valid, and what state the outputs are left in on failure.
As per coding guidelines, "Public C API headers in include/rac/ must document vtable operations, error codes, and lifecycle requirements".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-commons/include/rac/infrastructure/extraction/rac_extraction.h`
around lines 90 - 143, The header is missing lifecycle documentation for the
progress callback and input/output pointers; update the comments for
rac_extract_archive_native and rac_extraction_progress_fn to state that the
progress callback (rac_extraction_progress_fn) is invoked synchronously on the
calling thread during extraction, that user_data and the options pointer
(rac_extraction_options_t*) must remain valid and unchanged for the entire
duration of the rac_extract_archive_native call (and until the function returns)
because no background threads are used, and that out_result, if non-NULL, will
be populated only on successful completion (RAC_SUCCESS) and may be left
unmodified or only partially filled on failure—document the exact failure
semantics (e.g., zero/unchanged) so callers know how to handle it; also add a
brief note on rac_detect_archive_type that file_path and out_type must remain
valid only for the call and are not retained.
| // Workaround for sherpa-onnx std::once_flag issue: espeak_Initialize is | ||
| // only called internally on the very first SherpaOnnxCreateOfflineTts call. | ||
| // When switching TTS models with different data_dir, destroy and recreate | ||
| // the instance so the config (including data_dir) is applied correctly. | ||
| if (sherpa_tts_ && sherpa_tts_ != new_tts) { | ||
| SherpaOnnxDestroyOfflineTts(sherpa_tts_); | ||
| sherpa_tts_ = nullptr; | ||
| } | ||
| sherpa_tts_ = new_tts; | ||
|
|
There was a problem hiding this comment.
Prevent reload/unload from freeing sherpa_tts_ during synthesis.
Lines 1046-1074 copy the raw sherpa_tts_ pointer out of the mutex and use it after unlock. This reload path and unload_model() can destroy that same instance meanwhile, so concurrent model switches can turn tts_ptr into a dangling pointer mid-synthesis. Please wait for active_synthesis_count_ to reach zero before destroying/replacing the handle, or move the TTS instance behind shared ownership/ref-counting.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp` around lines 966
- 975, The code currently destroys/replaces sherpa_tts_ while syntheses may be
in progress, causing a dangling tts_ptr; modify the reload/unload logic (where
sherpa_tts_, new_tts, SherpaOnnxDestroyOfflineTts and unload_model() are used)
to either wait until active_synthesis_count_ == 0 before calling
SherpaOnnxDestroyOfflineTts and assigning sherpa_tts_ = new_tts, or convert
sherpa_tts_ to a shared/ref-counted handle (e.g., shared_ptr-like wrapper) so
in-progress synthesis holds a reference and destruction waits for last user;
ensure the mutex still protects pointer swap and that active_synthesis_count_ is
incremented/decremented around synthesis start/finish to coordinate the safe
destroy/swap.
| bool ONNXVAD::configure_vad(const VADConfig& config) { | ||
| std::lock_guard<std::mutex> lock(mutex_); | ||
| config_ = config; | ||
| return true; |
There was a problem hiding this comment.
configure_vad() still doesn't reconfigure the live detector.
This only updates config_, but process() keeps using the already-created sherpa_vad_, and load_model() is still the only place that applies threshold/window settings to Sherpa. That means callers can see get_vad_config() report values that were never applied to detection.
Also applies to: 1286-1288
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/backends/onnx/onnx_backend.cpp` around lines 1208
- 1211, configure_vad currently only updates config_ but does not apply those
settings to the live detector (sherpa_vad_), so process() continues using old
thresholds; modify configure_vad to acquire mutex_, set config_, and then apply
the new VAD parameters to the running detector — either by calling the
appropriate setter(s) on sherpa_vad_ or by reinitializing/reloading it similarly
to load_model() so threshold/window are applied; ensure get_vad_config() remains
consistent with the active detector and protect changes with mutex_ (refer to
configure_vad, load_model, process, get_vad_config, and the sherpa_vad_ member).
| void rac_lifecycle_release_service(rac_handle_t handle) { | ||
| if (handle == nullptr) { | ||
| return; | ||
| } | ||
|
|
||
| auto* mgr = static_cast<LifecycleManager*>(handle); | ||
| int prev = mgr->service_refcount.fetch_sub(1); | ||
| if (prev <= 1) { | ||
| mgr->service_cv.notify_all(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Refcount underflow is unprotected.
If rac_lifecycle_release_service is called without a prior acquire_service, the refcount underflows:
int prev = mgr->service_refcount.fetch_sub(1); // underflows to -1
if (prev <= 1) {
mgr->service_cv.notify_all(); // notifies even on underflow
}This corrupts state and could cause unload() to proceed prematurely on subsequent operations.
🛡️ Proposed fix: Add underflow check
void rac_lifecycle_release_service(rac_handle_t handle) {
if (handle == nullptr) {
return;
}
auto* mgr = static_cast<LifecycleManager*>(handle);
int prev = mgr->service_refcount.fetch_sub(1);
+ if (prev <= 0) {
+ // Underflow - caller released without acquiring
+ RAC_LOG_ERROR(mgr->logger_category.c_str(),
+ "release_service called without matching acquire (refcount was %d)", prev);
+ mgr->service_refcount.fetch_add(1); // Restore to prevent further corruption
+ return;
+ }
if (prev <= 1) {
mgr->service_cv.notify_all();
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-commons/src/core/capabilities/lifecycle_manager.cpp` around
lines 249 - 259, The refcount underflow in rac_lifecycle_release_service must be
prevented: modify rac_lifecycle_release_service to atomically decrement
service_refcount only if its current value is greater than zero (use an atomic
compare-exchange loop on LifecycleManager::service_refcount or load-and-compare
with compare_exchange_weak) and return immediately if it is already zero; when a
successful decrement yields previous==1 then call mgr->service_cv.notify_all(),
and optionally log or assert on an attempted release when count was zero to
surface misuse.
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart
Show resolved
Hide resolved
| final entries = calloc<Pointer<Utf8>>(count); | ||
| for (var i = 0; i < count; i++) { | ||
| final name = contents[i].uri.pathSegments.lastWhere((s) => s.isNotEmpty); | ||
| entries[i] = name.toNativeUtf8(); | ||
| } | ||
|
|
||
| outEntries.value = entries; | ||
| outCount.value = count; | ||
| return RacResultCode.success; | ||
| } catch (_) { | ||
| outEntries.value = nullptr; | ||
| outCount.value = 0; | ||
| return _errorFileNotFound; |
There was a problem hiding this comment.
Clean up partially allocated entry buffers on error.
Once entries and some entries[i] strings are allocated, any exception in the loop drops into the outer catch and leaks native memory. Free the successfully allocated elements before returning the error.
Suggested fix
final entries = calloc<Pointer<Utf8>>(count);
+ var allocated = 0;
for (var i = 0; i < count; i++) {
final name = contents[i].uri.pathSegments.lastWhere((s) => s.isNotEmpty);
entries[i] = name.toNativeUtf8();
+ allocated++;
}
outEntries.value = entries;
outCount.value = count;
return RacResultCode.success;
} catch (_) {
+ for (var i = 0; i < allocated; i++) {
+ calloc.free(entries[i]);
+ }
+ calloc.free(entries);
outEntries.value = nullptr;
outCount.value = 0;
return _errorFileNotFound;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 398 - 410, The loop allocates a contiguous buffer (entries via
calloc<Pointer<Utf8>>(count)) and individual Utf8 strings (name.toNativeUtf8())
but on exception the catch returns without freeing any of those, leaking native
memory; update the error path so that if an exception occurs during the for-loop
you iterate over the already-allocated indices (0..i-1) and free each entries[j]
(the pointers returned by toNativeUtf8()) and then free the entries buffer
itself (calloc.free(entries) or the appropriate allocator free), before setting
outEntries.value = nullptr, outCount.value = 0 and returning _errorFileNotFound;
apply this cleanup inside the existing try/catch around the loop that references
contents, entries, toNativeUtf8, outEntries, outCount and _errorFileNotFound.
| if (mkdir(path, 0755) == 0 || errno == EEXIST) { | ||
| return RAC_SUCCESS; | ||
| } | ||
| return RAC_ERROR_FILE_IO; |
There was a problem hiding this comment.
Only accept EEXIST when the target is actually a directory.
Line 53 currently treats any EEXIST as success, including the case where path is an existing file or symlink. That makes the callback lie about having created a directory and pushes the failure downstream.
Proposed fix
- if (mkdir(path, 0755) == 0 || errno == EEXIST) {
- return RAC_SUCCESS;
- }
+ if (mkdir(path, 0755) == 0) {
+ return RAC_SUCCESS;
+ }
+ if (errno == EEXIST) {
+ struct stat st;
+ if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
+ return RAC_SUCCESS;
+ }
+ }
return RAC_ERROR_FILE_IO;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (mkdir(path, 0755) == 0 || errno == EEXIST) { | |
| return RAC_SUCCESS; | |
| } | |
| return RAC_ERROR_FILE_IO; | |
| if (mkdir(path, 0755) == 0) { | |
| return RAC_SUCCESS; | |
| } | |
| if (errno == EEXIST) { | |
| struct stat st; | |
| if (lstat(path, &st) == 0 && S_ISDIR(st.st_mode)) { | |
| return RAC_SUCCESS; | |
| } | |
| } | |
| return RAC_ERROR_FILE_IO; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 53 - 56, The current mkdir handling in FileManagerBridge.cpp
returns RAC_SUCCESS whenever mkdir() succeeds or errno == EEXIST, which
incorrectly treats existing files/symlinks as a created directory; update the
logic so that when mkdir fails with errno == EEXIST you call stat/lstat on path
and verify S_ISDIR(st_mode) before returning RAC_SUCCESS, otherwise return
RAC_ERROR_FILE_IO; keep the existing return values (RAC_SUCCESS /
RAC_ERROR_FILE_IO) and use the same variables (path, errno) and functions
(mkdir) so the change is limited to adding the directory check.
| while ((entry = readdir(dir)) != nullptr && i < count) { | ||
| if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { | ||
| continue; | ||
| } | ||
| entries[i] = strdup(entry->d_name); | ||
| i++; | ||
| } | ||
| closedir(dir); | ||
|
|
||
| *outEntries = entries; | ||
| *outCount = i; | ||
| return RAC_SUCCESS; |
There was a problem hiding this comment.
Fail fast on partial strdup() failure.
Line 133 doesn't check strdup(). If one entry allocation fails, this returns RAC_SUCCESS with a null name in entries, which can crash the file-manager walk later.
Proposed fix
while ((entry = readdir(dir)) != nullptr && i < count) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
entries[i] = strdup(entry->d_name);
+ if (!entries[i]) {
+ for (size_t j = 0; j < i; ++j) {
+ free(entries[j]);
+ }
+ free(entries);
+ closedir(dir);
+ return RAC_ERROR_FILE_IO;
+ }
i++;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| while ((entry = readdir(dir)) != nullptr && i < count) { | |
| if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { | |
| continue; | |
| } | |
| entries[i] = strdup(entry->d_name); | |
| i++; | |
| } | |
| closedir(dir); | |
| *outEntries = entries; | |
| *outCount = i; | |
| return RAC_SUCCESS; | |
| while ((entry = readdir(dir)) != nullptr && i < count) { | |
| if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { | |
| continue; | |
| } | |
| entries[i] = strdup(entry->d_name); | |
| if (!entries[i]) { | |
| for (size_t j = 0; j < i; ++j) { | |
| free(entries[j]); | |
| } | |
| free(entries); | |
| closedir(dir); | |
| return RAC_ERROR_FILE_IO; | |
| } | |
| i++; | |
| } | |
| closedir(dir); | |
| *outEntries = entries; | |
| *outCount = i; | |
| return RAC_SUCCESS; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 129 - 140, The code in the readdir loop (using entry, strdup and
entries) doesn't check the result of strdup, so a failed allocation yields a
null entry and the function still returns RAC_SUCCESS; update the loop in
FileManagerBridge.cpp to check the return of strdup(entry->d_name) each
iteration, and on failure free any previously strdup'd strings and the entries
array, closedir(dir), set *outEntries and *outCount appropriately (e.g.,
null/0), and return a failure code (not RAC_SUCCESS) so callers don't receive a
partially populated entries array; ensure you reference entries, entry, strdup,
closedir, outEntries, outCount and replace the final RAC_SUCCESS return with an
error path when strdup fails.
| static int64_t posixGetAvailableSpace(void* /*userData*/) { | ||
| // Use root "/" on iOS, "/data" on Android | ||
| #if defined(ANDROID) || defined(__ANDROID__) | ||
| const char* mountPoint = "/data"; | ||
| #else | ||
| const char* mountPoint = "/"; | ||
| #endif | ||
|
|
||
| struct statvfs vfs; | ||
| if (statvfs(mountPoint, &vfs) != 0) return 0; | ||
| return static_cast<int64_t>(vfs.f_bavail) * static_cast<int64_t>(vfs.f_frsize); | ||
| } | ||
|
|
||
| static int64_t posixGetTotalSpace(void* /*userData*/) { | ||
| #if defined(ANDROID) || defined(__ANDROID__) | ||
| const char* mountPoint = "/data"; | ||
| #else | ||
| const char* mountPoint = "/"; | ||
| #endif | ||
|
|
||
| struct statvfs vfs; | ||
| if (statvfs(mountPoint, &vfs) != 0) return 0; | ||
| return static_cast<int64_t>(vfs.f_blocks) * static_cast<int64_t>(vfs.f_frsize); |
There was a problem hiding this comment.
Return the documented error sentinel from the space callbacks.
Lines 181 and 193 return 0 when statvfs() fails, but the callback contract is -1 on error. Returning 0 hides failures as valid storage values.
Proposed fix
struct statvfs vfs;
- if (statvfs(mountPoint, &vfs) != 0) return 0;
+ if (statvfs(mountPoint, &vfs) != 0) return -1;
return static_cast<int64_t>(vfs.f_bavail) * static_cast<int64_t>(vfs.f_frsize);
@@
struct statvfs vfs;
- if (statvfs(mountPoint, &vfs) != 0) return 0;
+ if (statvfs(mountPoint, &vfs) != 0) return -1;
return static_cast<int64_t>(vfs.f_blocks) * static_cast<int64_t>(vfs.f_frsize);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/FileManagerBridge.cpp`
around lines 172 - 194, The space callbacks posixGetAvailableSpace and
posixGetTotalSpace currently return 0 on statvfs() failure which violates the
callback contract (should return -1 on error) and can be mistaken for valid
space; update both functions so that when statvfs(mountPoint, &vfs) != 0 they
return a signed error sentinel (e.g., static_cast<int64_t>(-1)) instead of 0,
preserving the existing logic and types for the successful return path.
Fix critical 4-byte struct misalignment in RacModelInfoCStruct (missing supports_lora field) causing memory corruption. Add complete LoRA FFI bindings, bridge layer, public API, and types for Flutter feature parity with iOS/Kotlin.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart`:
- Around line 84-105: The LoRA methods (loadAdapter, removeAdapter,
clearAdapters, getLoadedAdapters) currently call
DartBridgeLLM.shared.getHandle() which can lazily create an empty LLM; add a
guard at the top of each of these methods to require a loaded base model before
proceeding (e.g. call a DartBridgeLLM.shared.isModelLoaded()/hasLoadedModel()
check or equivalent) and if no model is loaded throw a StateError with a clear
message instead of proceeding to getHandle(); for checkCompatibility, detect the
no-model case and return the incompatible/native error code (use the existing
RacResultCode/RAC_* conventions) rather than invoking native APIs. Ensure you
apply the same pattern to removeAdapter(), clearAdapters(), getLoadedAdapters(),
and checkCompatibility() so native calls are never reached when no base model is
loaded.
- Around line 349-369: Replace the native free lookup with the CRT-matching
rac_free and use it for all strdup'd strings: change the lookupFunction call
that currently resolves 'free' to resolve 'rac_free' (keeping the same typedefs
and the cFreeFn variable), and then call cFreeFn on entryPtr.ref.id, name,
description, downloadUrl, filename and on each non-null compatArrayPtr[i] in the
compat loop so all rac_strdup allocations are freed with rac_free.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart`:
- Around line 15-18: Replace the compile-time assert in the LoRAAdapterConfig
constructor with explicit runtime validation: in the LoRAAdapterConfig
constructor validate that path is not empty and throw an ArgumentError (or
ArgumentError.value) if it is, and also validate scale to ensure it is finite,
not NaN, and non-negative (throw ArgumentError if scale is NaN/Infinity or < 0);
perform these checks before assigning fields so invalid values never reach the
FFI boundary.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 71248ab6-9132-4fb2-b0d1-f6ca0c4be85d
📒 Files selected for processing (7)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_lora.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/types/types.dartsdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dart
🚧 Files skipped from review as they are similar to previous changes (1)
- sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart
| void loadAdapter(String adapterPath, double scale) { | ||
| final handle = DartBridgeLLM.shared.getHandle(); | ||
|
|
||
| final pathPtr = adapterPath.toNativeUtf8(); | ||
| try { | ||
| final lib = PlatformLoader.loadCommons(); | ||
| final fn = lib.lookupFunction< | ||
| Int32 Function(RacHandle, Pointer<Utf8>, Float), | ||
| int Function(RacHandle, Pointer<Utf8>, double)>( | ||
| 'rac_llm_component_load_lora', | ||
| ); | ||
|
|
||
| final result = fn(handle, pathPtr, scale); | ||
| if (result != RAC_SUCCESS) { | ||
| throw StateError( | ||
| 'Failed to load LoRA adapter: ${RacResultCode.getMessage(result)}', | ||
| ); | ||
| } | ||
| _logger.info('LoRA adapter loaded: $adapterPath (scale=$scale)'); | ||
| } finally { | ||
| calloc.free(pathPtr); | ||
| } |
There was a problem hiding this comment.
Require a loaded base model before calling these LoRA runtime APIs.
These paths all call DartBridgeLLM.shared.getHandle() directly. Because that lazily creates the component, the public LoRA API can reach native calls before any base model is loaded. That makes load/remove/clear/check fail late against an empty LLM, and getLoadedAdapters() can allocate one just to return [].
Suggested guard pattern
void loadAdapter(String adapterPath, double scale) {
+ if (!DartBridgeLLM.shared.isLoaded) {
+ throw StateError('No model loaded');
+ }
final handle = DartBridgeLLM.shared.getHandle(); List<LoRAAdapterInfo> getLoadedAdapters() {
+ if (!DartBridgeLLM.shared.isLoaded) return [];
final handle = DartBridgeLLM.shared.getHandle();Apply the same guard to removeAdapter() and clearAdapters(), and return an incompatible result from checkCompatibility() when no model is loaded.
Also applies to: 112-133, 140-157, 162-195, 198-236
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart`
around lines 84 - 105, The LoRA methods (loadAdapter, removeAdapter,
clearAdapters, getLoadedAdapters) currently call
DartBridgeLLM.shared.getHandle() which can lazily create an empty LLM; add a
guard at the top of each of these methods to require a loaded base model before
proceeding (e.g. call a DartBridgeLLM.shared.isModelLoaded()/hasLoadedModel()
check or equivalent) and if no model is loaded throw a StateError with a clear
message instead of proceeding to getHandle(); for checkCompatibility, detect the
no-model case and return the incompatible/native error code (use the existing
RacResultCode/RAC_* conventions) rather than invoking native APIs. Ensure you
apply the same pattern to removeAdapter(), clearAdapters(), getLoadedAdapters(),
and checkCompatibility() so native calls are never reached when no base model is
loaded.
| final cFreeFn = lib.lookupFunction< | ||
| Void Function(Pointer<Void>), | ||
| void Function(Pointer<Void>)>('free'); | ||
|
|
||
| // Free strdup'd strings inside the struct | ||
| if (entryPtr.ref.id != nullptr) cFreeFn(entryPtr.ref.id.cast()); | ||
| if (entryPtr.ref.name != nullptr) cFreeFn(entryPtr.ref.name.cast()); | ||
| if (entryPtr.ref.description != nullptr) { | ||
| cFreeFn(entryPtr.ref.description.cast()); | ||
| } | ||
| if (entryPtr.ref.downloadUrl != nullptr) { | ||
| cFreeFn(entryPtr.ref.downloadUrl.cast()); | ||
| } | ||
| if (entryPtr.ref.filename != nullptr) { | ||
| cFreeFn(entryPtr.ref.filename.cast()); | ||
| } | ||
| // Free strdup'd compatible model IDs | ||
| for (int i = 0; i < compatCount; i++) { | ||
| if (compatArrayPtr[i] != nullptr) { | ||
| cFreeFn(compatArrayPtr[i].cast()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && find . -name "dart_bridge_lora.dart" -type fRepository: RunanywhereAI/runanywhere-sdks
Length of output: 108
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && wc -l lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 110
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '340,380p' lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1601
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && grep -n "loadCommons\|rac_free\|rac_strdup" lib/native/dart_bridge_lora.dart | head -30Repository: RunanywhereAI/runanywhere-sdks
Length of output: 662
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && rg "loadCommons" -A 10 -B 2 lib/native/Repository: RunanywhereAI/runanywhere-sdks
Length of output: 50387
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '180,195p' lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 511
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '217,235p' lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 648
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '280,295p' lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 654
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere && sed -n '300,330p' lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1366
Use rac_free to match the allocation CRT.
Strings allocated via rac_strdup (lines 287–320) must be freed via rac_free, not raw free. The pattern is already used successfully elsewhere in the file (lines 188, 225). Using free creates a CRT mismatch and risks heap corruption if it resolves through a different runtime than rac_strdup.
Suggested fix
- final cFreeFn = lib.lookupFunction<
- Void Function(Pointer<Void>),
- void Function(Pointer<Void>)>('free');
+ final cFreeFn = lib.lookupFunction<
+ Void Function(Pointer<Void>),
+ void Function(Pointer<Void>)>('rac_free');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dart`
around lines 349 - 369, Replace the native free lookup with the CRT-matching
rac_free and use it for all strdup'd strings: change the lookupFunction call
that currently resolves 'free' to resolve 'rac_free' (keeping the same typedefs
and the cFreeFn variable), and then call cFreeFn on entryPtr.ref.id, name,
description, downloadUrl, filename and on each non-null compatArrayPtr[i] in the
compat loop so all rac_strdup allocations are freed with rac_free.
| LoRAAdapterConfig({ | ||
| required this.path, | ||
| this.scale = 1.0, | ||
| }) : assert(path.isNotEmpty, 'LoRA adapter path cannot be empty'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -type f -name "lora_types.dart" | head -20Repository: RunanywhereAI/runanywhere-sdks
Length of output: 244
🏁 Script executed:
fd "lora_types.dart"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 151
🏁 Script executed:
cat -n "sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart"Repository: RunanywhereAI/runanywhere-sdks
Length of output: 2877
🏁 Script executed:
rg -i "ffi|native" sdk/runanywhere-flutter/packages/runanywhere/lib --type dart -A 3 | head -50Repository: RunanywhereAI/runanywhere-sdks
Length of output: 5266
🏁 Script executed:
rg "LoRA|lora" sdk/runanywhere-flutter/packages/runanywhere/lib --type dart -B 2 -A 5Repository: RunanywhereAI/runanywhere-sdks
Length of output: 50388
🏁 Script executed:
rg "class.*Config\s*\{" sdk/runanywhere-flutter/packages/runanywhere/lib --type dart -A 10 | head -60Repository: RunanywhereAI/runanywhere-sdks
Length of output: 6272
🌐 Web query:
Dart assert behavior release builds production Flutter
💡 Result:
-
In Flutter, build modes control whether
assert(...)runs:- Debug mode: assertions are enabled. [1]
- Release mode (production builds): assertions are disabled. [1]
- Profile mode: is “similar to release mode”; in practice, Flutter’s engine artifacts/documentation also treat it as product mode with asserts disabled. [1][2]
-
In Dart, when assertions are disabled, the entire
assert(...)is removed—the condition (and any side effects inside it) is not evaluated. So you must not put required logic inside anassert. [1] -
If you need a check in production, use explicit runtime logic (e.g.,
if (...) throw ...;), notassert. -
If you’re running/compiling Dart yourself and want assertions on, use
--enable-asserts. [3]
Sources:
[1] Flutter docs: “Flutter’s build modes” (assertions enabled in debug, disabled in release) (docs.flutter.dev)
[2] Flutter engine doc noting asserts disabled in product/release-like builds (chromium.googlesource.com)
[3] Dart CLI docs (--enable-asserts) (dart.dev)
Citations:
- 1: https://docs.flutter.dev/testing/build-modes
- 2: https://chromium.googlesource.com/external/github.com/flutter/engine/%2B/refs/tags/3.27.3/docs/JIT-Release-Modes.md?utm_source=openai
- 3: https://dart.dev/tools/dart-compile?utm_source=openai
🏁 Script executed:
sed -n '75,130p' sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_lora.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1841
Replace assert with explicit runtime validation for LoRAAdapterConfig.
assert is stripped in Flutter release builds, so an empty path will reach the FFI boundary unvalidated in production. Replace with ArgumentError and add validation for scale to prevent NaN/infinity/negative values.
Suggested fix
LoRAAdapterConfig({
required this.path,
this.scale = 1.0,
- }) : assert(path.isNotEmpty, 'LoRA adapter path cannot be empty');
+ }) {
+ if (path.isEmpty) {
+ throw ArgumentError.value(
+ path,
+ 'path',
+ 'LoRA adapter path cannot be empty',
+ );
+ }
+ if (!scale.isFinite || scale < 0) {
+ throw ArgumentError.value(
+ scale,
+ 'scale',
+ 'LoRA scale must be finite and non-negative',
+ );
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| LoRAAdapterConfig({ | |
| required this.path, | |
| this.scale = 1.0, | |
| }) : assert(path.isNotEmpty, 'LoRA adapter path cannot be empty'); | |
| LoRAAdapterConfig({ | |
| required this.path, | |
| this.scale = 1.0, | |
| }) { | |
| if (path.isEmpty) { | |
| throw ArgumentError.value( | |
| path, | |
| 'path', | |
| 'LoRA adapter path cannot be empty', | |
| ); | |
| } | |
| if (!scale.isFinite || scale < 0) { | |
| throw ArgumentError.value( | |
| scale, | |
| 'scale', | |
| 'LoRA scale must be finite and non-negative', | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/lora_types.dart`
around lines 15 - 18, Replace the compile-time assert in the LoRAAdapterConfig
constructor with explicit runtime validation: in the LoRAAdapterConfig
constructor validate that path is not empty and throw an ArgumentError (or
ArgumentError.value) if it is, and also validate scale to ensure it is finite,
not NaN, and non-negative (throw ArgumentError if scale is NaN/Infinity or < 0);
perform these checks before assigning fields so invalid values never reach the
FFI boundary.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart (1)
835-840:⚠️ Potential issue | 🔴 CriticalDuplicate variable declaration causes compilation error.
Line 835 declares
entriesPtrwithcallocand line 836 re-declares it withmalloc. Remove the originalcallocline to fix.🐛 Proposed fix
// Allocate array of string pointers - final entriesPtr = calloc<Pointer<Utf8>>(entries.length); final entriesPtr = malloc<Pointer<Utf8>>(entries.length); for (var i = 0; i < entries.length; i++) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart` around lines 835 - 840, Remove the duplicate declaration of entriesPtr: delete the calloc<Pointer<Utf8>>(entries.length) line and keep the malloc<Pointer<Utf8>>(entries.length) allocation so entriesPtr is declared only once; ensure the loop that writes entriesPtr[i] and the assignment outEntries.value = entriesPtr remain unchanged and that the malloc symbol used in dart_bridge_model_registry.dart matches existing memory allocation conventions in the surrounding code.
♻️ Duplicate comments (3)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart (3)
275-278:⚠️ Potential issue | 🟠 MajorFail closed when storage availability can't be determined.
Returning
trueon a null result allows callers to proceed exactly when the bridge is missing or the native check failed.Suggested fix
static bool checkStorage(int requiredBytes) { final result = checkStorageAvailability(requiredBytes); - if (result == null) return true; // Default to available if check fails + if (result == null) return false; return result.isAvailable; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart` around lines 275 - 278, The checkStorage method currently treats a null result from checkStorageAvailability as available; change it to fail closed: if checkStorageAvailability(requiredBytes) returns null, return false instead of true so callers do not proceed when the native bridge is missing or the check failed; update the null-handling branch in checkStorage (referencing checkStorage and checkStorageAvailability and the result.isAvailable usage) to return false and keep the normal return result.isAvailable for non-null results.
408-420:⚠️ Potential issue | 🟡 MinorClean up partially allocated entry buffers on error.
If the loop throws after allocating
entriesand a fewentries[i]strings, the catch path leaks both the array and the already-created strings.Suggested fix
- final entries = calloc<Pointer<Utf8>>(count); - for (var i = 0; i < count; i++) { + final entries = calloc<Pointer<Utf8>>(count); + var allocated = 0; + for (var i = 0; i < count; i++) { final name = contents[i].uri.pathSegments.lastWhere((s) => s.isNotEmpty); - entries[i] = name.toNativeUtf8(); + entries[i] = name.toNativeUtf8(allocator: calloc); + allocated++; } @@ - } catch (_) { + } catch (_) { + for (var i = 0; i < allocated; i++) { + calloc.free(entries[i]); + } + calloc.free(entries); outEntries.value = nullptr; outCount.value = 0; return _errorFileNotFound; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart` around lines 408 - 420, The catch path currently leaks memory because the calloc'd entries array and any previously created Utf8 buffers (via toNativeUtf8) are not freed; modify the function so that if an exception occurs after allocating entries, you iterate from 0 to the last successfully initialized index and free each entries[i] (using malloc.free or appropriate freeUtf8), then free the entries array itself before setting outEntries.value = nullptr and outCount.value = 0 and returning _errorFileNotFound; ensure the same cleanup logic is used for any early-return error paths so that the calloc<Pointer<Utf8>>(count) allocation and any populated entries[i] are always released (reference symbols: entries, calloc<Pointer<Utf8>>, toNativeUtf8, outEntries, outCount, _errorFileNotFound, RacResultCode.success).
104-106:⚠️ Potential issue | 🟠 MajorDon't collapse native file-manager failures into
0.These methods ignore the native return code and always read the out-param, so an FFI failure is indistinguishable from a real empty directory/cache. Surface the error, or return
null, and only read the size when the native call succeeds.Fix pattern
- fn(_callbacksPtr!, pathPtr, sizePtr); - return sizePtr.value; + final result = fn(_callbacksPtr!, pathPtr, sizePtr); + if (result != RacResultCode.success) { + throw StateError('rac_file_manager_calculate_dir_size failed: $result'); + } + return sizePtr.value;Also applies to: 124-126, 163-165
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart` around lines 104 - 106, The current try-block calls the native callback fn(_callbacksPtr!, pathPtr, sizePtr) and always returns sizePtr.value, collapsing native failures into 0; change the logic to capture and check the native return code from fn, and only read and return sizePtr.value when the native call indicates success (otherwise return null or propagate the error). Update the occurrence that uses fn, _callbacksPtr, pathPtr, sizePtr (and the analogous sites mentioned for lines 124-126 and 163-165) so they inspect the native return value before reading the out-parameter, returning null or throwing when the native call failed.
🧹 Nitpick comments (2)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart (1)
176-176: Replace the raw archive-structure sentinel with a named bridge constant.Hardcoding
99here creates a silent Dart↔C++ enum dependency. If the native value ever changes, auto-detection breaks without any compiler help. Please expose/use a named constant alongside the existingmodel.format.toC()bridge.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart` at line 176, The code uses a hardcoded sentinel value `99` for `structure` which creates a brittle Dart↔C++ enum dependency; add and use a named bridge constant (e.g. `ARCHIVE_STRUCTURE_UNKNOWN` or `RAC_ARCHIVE_STRUCTURE_UNKNOWN`) exported from the same bridge surface as `model.format.toC()` and replace `structure: 99` with `structure: <BRIDGE_CONSTANT>` so the Dart side references the named constant instead of the raw number; ensure the constant name matches the native enum sentinel and is documented alongside `model.format.toC()` so changes in native values won’t silently break auto-detection.sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart (1)
377-381: Replace the raw archive-structure sentinel with a shared constant.
99is a native-side enum value embedded directly in Dart. If that value ever drifts in the C API, model-path discovery here will silently use the wrong mode. Please export a named constant through the bridge/FFI layer and use that symbol instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart` around lines 377 - 381, The call to DartBridgeDownload.findModelPathAfterExtraction uses a hardcoded sentinel "99" for the structure argument which can drift if the native enum changes; export a named constant from the bridge/FFI layer (e.g., RAC_ARCHIVE_STRUCTURE_UNKNOWN) and replace the literal with that constant so the call becomes: DartBridgeDownload.findModelPathAfterExtraction(extractedDir: destDir, structure: BridgeConstants.RAC_ARCHIVE_STRUCTURE_UNKNOWN, framework: framework.toC(), format: format.toC()); ensure the constant is defined and exported by the Dart FFI bridge module and referenced here instead of the raw 99.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`:
- Around line 102-109: The code calls toNativeUtf8() (producing pathPtr) but
frees the pointer with calloc.free(), violating allocator symmetry; update each
toNativeUtf8() call in dart_bridge_file_manager.dart to specify allocator:
calloc so the pointer is allocated with calloc and can be freed with
calloc.free() (apply this change for all occurrences that create pathPtr and
later call calloc.free(), e.g., the places that define pathPtr/sizePtr and later
free them).
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`:
- Around line 174-181: The current resolveModelFilePath implementation masks
extraction/layout failures by returning modelFolder when
DartBridgeDownload.findModelPathAfterExtraction returns null; change the code to
stop falling back and return the resolved value directly (replace "return
resolved ?? modelFolder;" with "return resolved;"), ensure
resolveModelFilePath's return type is nullable if not already, and update
callers to handle a null result from
DartBridgeDownload.findModelPathAfterExtraction/resolveModelFilePath so
single-file formats (e.g., GGUF) and extraction errors are not misreported as a
directory path.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart`:
- Around line 173-177: The UTF-8 conversion calls (e.g., when setting
cConfig.ref.promptTemplate, cConfig.ref.embeddingConfigJson, and
cConfig.ref.llmConfigJson) use toNativeUtf8() which defaults to malloc, but the
code later frees those pointers with calloc.free(); change these to
toNativeUtf8(allocator: calloc) so the allocator matches the cleanup for all
UTF-8 strings in the function (apply the same change to the other occurrences
noted).
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dart`:
- Around line 96-99: The code marks the module registered unconditionally after
calling DartBridgeRAG.shared.register(); change the flow so _isRegistered is
only set to true (and _logger.info called) when DartBridgeRAG.shared.register()
confirms success—either by returning a success boolean or by throwing on
failure; if register currently only logs and returns, update
DartBridgeRAG.register() to return/throw success/failure and then check that
result here, leaving _isRegistered false and logging an error when the bridge
did not start successfully. Ensure you reference
DartBridgeRAG.shared.register(), _isRegistered, and the surrounding registration
logic when making the change.
---
Outside diff comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart`:
- Around line 835-840: Remove the duplicate declaration of entriesPtr: delete
the calloc<Pointer<Utf8>>(entries.length) line and keep the
malloc<Pointer<Utf8>>(entries.length) allocation so entriesPtr is declared only
once; ensure the loop that writes entriesPtr[i] and the assignment
outEntries.value = entriesPtr remain unchanged and that the malloc symbol used
in dart_bridge_model_registry.dart matches existing memory allocation
conventions in the surrounding code.
---
Duplicate comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`:
- Around line 275-278: The checkStorage method currently treats a null result
from checkStorageAvailability as available; change it to fail closed: if
checkStorageAvailability(requiredBytes) returns null, return false instead of
true so callers do not proceed when the native bridge is missing or the check
failed; update the null-handling branch in checkStorage (referencing
checkStorage and checkStorageAvailability and the result.isAvailable usage) to
return false and keep the normal return result.isAvailable for non-null results.
- Around line 408-420: The catch path currently leaks memory because the
calloc'd entries array and any previously created Utf8 buffers (via
toNativeUtf8) are not freed; modify the function so that if an exception occurs
after allocating entries, you iterate from 0 to the last successfully
initialized index and free each entries[i] (using malloc.free or appropriate
freeUtf8), then free the entries array itself before setting outEntries.value =
nullptr and outCount.value = 0 and returning _errorFileNotFound; ensure the same
cleanup logic is used for any early-return error paths so that the
calloc<Pointer<Utf8>>(count) allocation and any populated entries[i] are always
released (reference symbols: entries, calloc<Pointer<Utf8>>, toNativeUtf8,
outEntries, outCount, _errorFileNotFound, RacResultCode.success).
- Around line 104-106: The current try-block calls the native callback
fn(_callbacksPtr!, pathPtr, sizePtr) and always returns sizePtr.value,
collapsing native failures into 0; change the logic to capture and check the
native return code from fn, and only read and return sizePtr.value when the
native call indicates success (otherwise return null or propagate the error).
Update the occurrence that uses fn, _callbacksPtr, pathPtr, sizePtr (and the
analogous sites mentioned for lines 124-126 and 163-165) so they inspect the
native return value before reading the out-parameter, returning null or throwing
when the native call failed.
---
Nitpick comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dart`:
- Around line 377-381: The call to
DartBridgeDownload.findModelPathAfterExtraction uses a hardcoded sentinel "99"
for the structure argument which can drift if the native enum changes; export a
named constant from the bridge/FFI layer (e.g., RAC_ARCHIVE_STRUCTURE_UNKNOWN)
and replace the literal with that constant so the call becomes:
DartBridgeDownload.findModelPathAfterExtraction(extractedDir: destDir,
structure: BridgeConstants.RAC_ARCHIVE_STRUCTURE_UNKNOWN, framework:
framework.toC(), format: format.toC()); ensure the constant is defined and
exported by the Dart FFI bridge module and referenced here instead of the raw
99.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`:
- Line 176: The code uses a hardcoded sentinel value `99` for `structure` which
creates a brittle Dart↔C++ enum dependency; add and use a named bridge constant
(e.g. `ARCHIVE_STRUCTURE_UNKNOWN` or `RAC_ARCHIVE_STRUCTURE_UNKNOWN`) exported
from the same bridge surface as `model.format.toC()` and replace `structure: 99`
with `structure: <BRIDGE_CONSTANT>` so the Dart side references the named
constant instead of the raw number; ensure the constant name matches the native
enum sentinel and is documented alongside `model.format.toC()` so changes in
native values won’t silently break auto-detection.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d33f0c08-b945-49c2-95a5-df4bbd4372b3
📒 Files selected for processing (9)
sdk/runanywhere-flutter/packages/runanywhere/lib/infrastructure/download/download_service.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_rag.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/types/rag_types.dartsdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dart
💤 Files with no reviewable changes (1)
- sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/rag_types.dart
| final pathPtr = path.toNativeUtf8(); | ||
| final sizePtr = calloc<Int64>(); | ||
| try { | ||
| fn(_callbacksPtr!, pathPtr, sizePtr); | ||
| return sizePtr.value; | ||
| } finally { | ||
| calloc.free(pathPtr); | ||
| calloc.free(sizePtr); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Dart package:ffi String.toNativeUtf8() allocate with malloc by default, and must the returned pointer be freed with the same allocator instead of calloc.free()?
💡 Result:
Yes.
String.toNativeUtf8()is declared astoNativeUtf8({Allocator allocator = malloc}), so it allocates withmallocby default. [1]- The returned pointer should be freed with the same allocator you used to allocate it (i.e.,
allocator.free(ptr)). With the default, that meansmalloc.free(ptr). [1][2]
In practice, calloc.free(ptr) often “works” because both allocators typically route free to the same underlying native deallocator, but relying on that is brittle—especially if you ever pass a different/custom allocator—so prefer malloc.free (or allocator.free). [1][2]
Sources: [1] StringUtf8Pointer.toNativeUtf8 API docs, [2] malloc allocator docs.
Free these UTF-8 buffers with the allocator that created them.
toNativeUtf8() defaults to malloc, but every cleanup path uses calloc.free(). The pointer must be freed with the same allocator that allocated it. While both allocators typically route to the same underlying deallocator, this mismatch violates the allocator contract and is brittle—especially if custom allocators are ever used.
Specify allocator: calloc in toNativeUtf8() calls to match the corresponding calloc.free() cleanup, or switch cleanup to malloc.free().
Fix pattern
- final pathPtr = path.toNativeUtf8();
+ final pathPtr = path.toNativeUtf8(allocator: calloc);
...
- final modelIdPtr = modelId.toNativeUtf8();
+ final modelIdPtr = modelId.toNativeUtf8(allocator: calloc);
...
- entries[i] = name.toNativeUtf8();
+ entries[i] = name.toNativeUtf8(allocator: calloc);Applies to lines 102–109, 181–190, 205–212, 291–296, 408–429.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_file_manager.dart`
around lines 102 - 109, The code calls toNativeUtf8() (producing pathPtr) but
frees the pointer with calloc.free(), violating allocator symmetry; update each
toNativeUtf8() call in dart_bridge_file_manager.dart to specify allocator:
calloc so the pointer is allocated with calloc and can be freed with
calloc.free() (apply this change for all occurrences that create pathPtr and
later call calloc.free(), e.g., the places that define pathPtr/sizePtr and later
free them).
| final resolved = DartBridgeDownload.findModelPathAfterExtraction( | ||
| extractedDir: modelFolder, | ||
| structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect | ||
| framework: _frameworkToCValue(model.framework), | ||
| format: model.format.toC(), | ||
| ); | ||
|
|
||
| // Scan for any subdirectory with model files | ||
| final dir = Directory(modelFolder); | ||
| if (dir.existsSync()) { | ||
| for (final entity in dir.listSync()) { | ||
| if (entity is Directory && _hasONNXModelFiles(entity.path)) { | ||
| _logger.info('Found ONNX model in subdirectory: ${entity.path}'); | ||
| return entity.path; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Fallback | ||
| _logger.warning('No ONNX model files found, using: $modelFolder'); | ||
| return modelFolder; | ||
| } | ||
|
|
||
| /// Check if directory contains ONNX model files | ||
| bool _hasONNXModelFiles(String directory) { | ||
| final dir = Directory(directory); | ||
| if (!dir.existsSync()) return false; | ||
|
|
||
| try { | ||
| return dir.listSync().any((entity) { | ||
| if (entity is! File) return false; | ||
| final name = entity.path.toLowerCase(); | ||
| return name.endsWith('.onnx') || | ||
| name.endsWith('.ort') || | ||
| name.contains('encoder') || | ||
| name.contains('decoder') || | ||
| name.contains('tokens'); | ||
| }); | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /// Resolve single-file model path (LlamaCpp .gguf files) | ||
| String? _resolveSingleFileModelPath(String modelFolder, ModelInfo model) { | ||
| final dir = Directory(modelFolder); | ||
| if (!dir.existsSync()) { | ||
| _logger.warning('Model folder does not exist: $modelFolder'); | ||
| return null; | ||
| } | ||
|
|
||
| // Find the model file | ||
| try { | ||
| for (final entity in dir.listSync()) { | ||
| if (entity is File) { | ||
| final name = entity.path.toLowerCase(); | ||
| if (name.endsWith('.gguf') || name.endsWith('.bin')) { | ||
| _logger.info('Found model file: ${entity.path}'); | ||
| return entity.path; | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| _logger.warning('Error scanning model folder: $e'); | ||
| } | ||
|
|
||
| _logger.warning('No model file found in: $modelFolder'); | ||
| return null; | ||
| return resolved ?? modelFolder; |
There was a problem hiding this comment.
Do not fall back to the model directory when native resolution fails.
resolveModelFilePath() is documented as returning the loadable model path, but Line 181 returns the container directory for every unresolved case. That masks extraction/layout failures and is wrong for single-file formats like GGUF, where callers will receive a directory instead of a model file.
Suggested fix
final resolved = DartBridgeDownload.findModelPathAfterExtraction(
extractedDir: modelFolder,
structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect
framework: _frameworkToCValue(model.framework),
format: model.format.toC(),
);
- return resolved ?? modelFolder;
+ if (resolved != null) {
+ return resolved;
+ }
+
+ _logger.warning(
+ 'Unable to resolve extracted model path for ${model.id} in $modelFolder',
+ );
+ return null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final resolved = DartBridgeDownload.findModelPathAfterExtraction( | |
| extractedDir: modelFolder, | |
| structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect | |
| framework: _frameworkToCValue(model.framework), | |
| format: model.format.toC(), | |
| ); | |
| // Scan for any subdirectory with model files | |
| final dir = Directory(modelFolder); | |
| if (dir.existsSync()) { | |
| for (final entity in dir.listSync()) { | |
| if (entity is Directory && _hasONNXModelFiles(entity.path)) { | |
| _logger.info('Found ONNX model in subdirectory: ${entity.path}'); | |
| return entity.path; | |
| } | |
| } | |
| } | |
| // Fallback | |
| _logger.warning('No ONNX model files found, using: $modelFolder'); | |
| return modelFolder; | |
| } | |
| /// Check if directory contains ONNX model files | |
| bool _hasONNXModelFiles(String directory) { | |
| final dir = Directory(directory); | |
| if (!dir.existsSync()) return false; | |
| try { | |
| return dir.listSync().any((entity) { | |
| if (entity is! File) return false; | |
| final name = entity.path.toLowerCase(); | |
| return name.endsWith('.onnx') || | |
| name.endsWith('.ort') || | |
| name.contains('encoder') || | |
| name.contains('decoder') || | |
| name.contains('tokens'); | |
| }); | |
| } catch (e) { | |
| return false; | |
| } | |
| } | |
| /// Resolve single-file model path (LlamaCpp .gguf files) | |
| String? _resolveSingleFileModelPath(String modelFolder, ModelInfo model) { | |
| final dir = Directory(modelFolder); | |
| if (!dir.existsSync()) { | |
| _logger.warning('Model folder does not exist: $modelFolder'); | |
| return null; | |
| } | |
| // Find the model file | |
| try { | |
| for (final entity in dir.listSync()) { | |
| if (entity is File) { | |
| final name = entity.path.toLowerCase(); | |
| if (name.endsWith('.gguf') || name.endsWith('.bin')) { | |
| _logger.info('Found model file: ${entity.path}'); | |
| return entity.path; | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| _logger.warning('Error scanning model folder: $e'); | |
| } | |
| _logger.warning('No model file found in: $modelFolder'); | |
| return null; | |
| return resolved ?? modelFolder; | |
| final resolved = DartBridgeDownload.findModelPathAfterExtraction( | |
| extractedDir: modelFolder, | |
| structure: 99, // RAC_ARCHIVE_STRUCTURE_UNKNOWN - auto-detect | |
| framework: _frameworkToCValue(model.framework), | |
| format: model.format.toC(), | |
| ); | |
| if (resolved != null) { | |
| return resolved; | |
| } | |
| _logger.warning( | |
| 'Unable to resolve extracted model path for ${model.id} in $modelFolder', | |
| ); | |
| return null; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_paths.dart`
around lines 174 - 181, The current resolveModelFilePath implementation masks
extraction/layout failures by returning modelFolder when
DartBridgeDownload.findModelPathAfterExtraction returns null; change the code to
stop falling back and return the resolved value directly (replace "return
resolved ?? modelFolder;" with "return resolved;"), ensure
resolveModelFilePath's return type is nullable if not already, and update
callers to handle a null result from
DartBridgeDownload.findModelPathAfterExtraction/resolveModelFilePath so
single-file formats (e.g., GGUF) and extraction errors are not misreported as a
directory path.
| cConfig.ref.embeddingConfigJson = config.embeddingConfigJSON != null | ||
| ? config.embeddingConfigJSON!.toNativeUtf8() | ||
| : nullptr; | ||
| cConfig.ref.llmConfigJson = config.llmConfigJson != null | ||
| ? config.llmConfigJson!.toNativeUtf8() | ||
| cConfig.ref.llmConfigJson = config.llmConfigJSON != null | ||
| ? config.llmConfigJSON!.toNativeUtf8() |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Dart package:ffi String.toNativeUtf8() allocate with malloc by default, and must the returned pointer be freed with the same allocator instead of calloc.free()?
💡 Result:
Yes.
String.toNativeUtf8()(frompackage:ffi/ffi.dart) has an optionalallocatorparameter that defaults tomalloc({Allocator allocator = malloc}), and it returns an allocator-allocated pointer. [1]- Therefore, if you use the default, free it with the same allocator:
malloc.free(ptr). [1][2]
Using calloc.free(ptr) is typically equivalent on the supported platforms because both allocators ultimately release memory with the platform “free” routine (POSIX: free; Windows: CoTaskMemFree). [2][3] But the correct rule is: free with the same allocator you allocated with (and this matters if you pass a different/custom allocator). [1]
Sources
[1] https://pub.dev/documentation/ffi/latest/ffi/StringUtf8Pointer/toNativeUtf8.html
[2] https://pub.dev/documentation/ffi/latest/ffi/MallocAllocator-class.html
[3] https://pub.dev/documentation/ffi/latest/ffi/CallocAllocator-class.html
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere/lib/native && wc -l dart_bridge_rag.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 98
🏁 Script executed:
cd sdk/runanywhere-flutter/packages/runanywhere/lib/native && sed -n '170,210p' dart_bridge_rag.dartRepository: RunanywhereAI/runanywhere-sdks
Length of output: 1448
Match the allocator for these JSON config buffers.
toNativeUtf8() uses malloc by default, but these pointers are released with calloc.free(). While this may work in practice because both allocators typically use the same underlying platform free routine, it violates the allocator contract and is not guaranteed to be safe across all platforms. Use allocator: calloc here to match the cleanup, and apply the same fix to promptTemplate (line 171) and other UTF-8 strings in this function that follow the same pattern.
Fix pattern
- cConfig.ref.embeddingConfigJson = config.embeddingConfigJSON != null
- ? config.embeddingConfigJSON!.toNativeUtf8()
+ cConfig.ref.embeddingConfigJson = config.embeddingConfigJSON != null
+ ? config.embeddingConfigJSON!.toNativeUtf8(allocator: calloc)
: nullptr;
- cConfig.ref.llmConfigJson = config.llmConfigJSON != null
- ? config.llmConfigJSON!.toNativeUtf8()
+ cConfig.ref.llmConfigJson = config.llmConfigJSON != null
+ ? config.llmConfigJSON!.toNativeUtf8(allocator: calloc)
: nullptr;Also applies to: 171, 197–201
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart`
around lines 173 - 177, The UTF-8 conversion calls (e.g., when setting
cConfig.ref.promptTemplate, cConfig.ref.embeddingConfigJson, and
cConfig.ref.llmConfigJson) use toNativeUtf8() which defaults to malloc, but the
code later frees those pointers with calloc.free(); change these to
toNativeUtf8(allocator: calloc) so the allocator matches the cleanup for all
UTF-8 strings in the function (apply the same change to the other occurrences
noted).
| DartBridgeRAG.shared.register(); | ||
|
|
||
| _isRegistered = true; | ||
| _logger.info('RAG backend registered successfully'); |
There was a problem hiding this comment.
Only mark the module registered after the bridge confirms success.
DartBridgeRAG.shared.register() does not throw on a non-success native return; it just logs and returns. This path still flips _isRegistered to true, so callers can bypass the registration guard even though the backend never came up.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dart`
around lines 96 - 99, The code marks the module registered unconditionally after
calling DartBridgeRAG.shared.register(); change the flow so _isRegistered is
only set to true (and _logger.info called) when DartBridgeRAG.shared.register()
confirms success—either by returning a success boolean or by throwing on
failure; if register currently only logs and returns, update
DartBridgeRAG.register() to return/throw success/failure and then check that
result here, leaving _isRegistered false and logging an error when the bridge
did not start successfully. Ensure you reference
DartBridgeRAG.shared.register(), _isRegistered, and the surrounding registration
logic when making the change.
Description
Brief description of the changes made.
Type of Change
Testing
Platform-Specific Testing (check all that apply)
Swift SDK / iOS Sample:
Kotlin SDK / Android Sample:
Flutter SDK / Flutter Sample:
React Native SDK / React Native Sample:
Playground:
Web SDK / Web Sample:
Labels
Please add the appropriate label(s):
SDKs:
Swift SDK- Changes to Swift SDK (sdk/runanywhere-swift)Kotlin SDK- Changes to Kotlin SDK (sdk/runanywhere-kotlin)Flutter SDK- Changes to Flutter SDK (sdk/runanywhere-flutter)React Native SDK- Changes to React Native SDK (sdk/runanywhere-react-native)Web SDK- Changes to Web SDK (sdk/runanywhere-web)Commons- Changes to shared native code (sdk/runanywhere-commons)Sample Apps:
iOS Sample- Changes to iOS example app (examples/ios)Android Sample- Changes to Android example app (examples/android)Flutter Sample- Changes to Flutter example app (examples/flutter)React Native Sample- Changes to React Native example app (examples/react-native)Web Sample- Changes to Web example app (examples/web)Checklist
Screenshots
Attach relevant UI screenshots for changes (if applicable):
Summary by CodeRabbit
New Features
Improvements
Bug Fixes / Tests
Chores
Greptile Summary
This PR consolidates platform-specific archive extraction (previously in Swift SWCompression, Kotlin Commons Compress, and Flutter's
archivepackage) into a single native C++ implementation usinglibarchive, and centralizes file management and download orchestration business logic that was duplicated across all four SDK platforms. It also introduces a native RAG pipeline (ONNX embedding provider + USearch vector store), cache-line-optimized energy VAD, and a significantly improved structured logging system with atomic level-filtering.Key changes:
rac_extraction.cpp: Streaming libarchive extraction with zip-slip protection, macOS resource fork skipping, and safe symlink handling — well implemented.download_orchestrator.cpp: Single-file orchestration (rac_download_orchestrate) is sound; however, the multi-file variant (rac_download_orchestrate_multi) has a critical race condition where downloads are started asynchronously but the task is marked complete immediately after the loop, and transfer failures are silently ignored.file_manager.cpp: Clean centralization of recursive directory sizing and folder lifecycle management via platform I/O callbacks.onnx_embedding_provider.cppadds WordPiece tokenization with NEON SIMD path and LRU token cache;vector_store_usearch.cppadds an f16-quantized USearch index. Theload()method on the vector store does not clear existing index data before loading, which can produce an inconsistent index/metadata state.Confidence Score: 3/5
rac_download_orchestrate_multitreats async HTTP launches as synchronous completions, silently dropping transfer-phase failures and marking the task done prematurely, and (2)VectorStoreUSearch::load()can produce inconsistent index/metadata state by not clearing existing data first. These are non-trivial correctness issues in new code paths.sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp(multi-file race condition and incorrect success return) andsdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp(load without prior clear).Important Files Changed
rac_download_orchestrate_multihas a critical race: downloads are started asynchronously viarac_http_downloadyet the task is marked complete immediately after the loop, andany_failedonly reflects launch errors, not transfer failures. The function also incorrectly returnsRAC_SUCCESSwhen required files fail to start.load()method does not clear existing index data before loading, which can leave the USearch index out of sync with the newly deserialized metadata maps.embed()andembed_batch()is functional but fragile due to dual-path mutex acquisition.rac_download_requires_extraction,rac_download_compute_destination, andrac_find_model_path_after_extraction. Uses shell commands (system("rm -rf ...")) inside test helpers — functional but worth noting for environments where shell access is restricted.Sequence Diagram
sequenceDiagram participant SDK as Swift/Kotlin/Flutter/RN SDK participant Orch as DownloadOrchestrator (C++) participant DM as DownloadManager participant HTTP as Platform HTTP Adapter participant Ext as Extraction (libarchive) participant FM as FileManager (C++) SDK->>Orch: rac_download_orchestrate(model_id, url, ...) Orch->>DM: rac_download_manager_start(task_id) Orch->>HTTP: rac_http_download(url, dest, callbacks) HTTP-->>Orch: orchestrate_http_progress(bytes, total) Orch->>DM: rac_download_manager_update_progress() HTTP-->>Orch: orchestrate_http_complete(result, path) alt needs_extraction Orch->>DM: mark_complete (→ EXTRACTING state) Orch->>Ext: rac_extract_archive_native(archive, dest_dir) Ext-->>Orch: extraction_result Orch->>Orch: rac_find_model_path_after_extraction() Orch->>Orch: delete_file(archive) Orch->>DM: mark_extraction_complete(final_path) else direct download Orch->>DM: mark_complete(final_path) end Orch->>SDK: user_complete_callback(task_id, result, path) Note over SDK,FM: File management (separate flow) SDK->>FM: rac_file_manager_calculate_storage_usage() FM->>FM: recursive directory traversal via callbacks FM-->>SDK: storage_info (used/available/warning)Comments Outside Diff (2)
sdk/runanywhere-commons/src/features/rag/vector_store_usearch.cpp, line 317-373 (link)load()does not clear existing index data before loadingload()callsindex_.load(path.c_str())while holdingmutex_, but it does not callindex_.clear()first. If theVectorStoreUSearchalready contains entries (from prioradd_chunkcalls or a previousload), the USearch index may retain stale vectors while the metadata maps (chunks_,id_to_key_) are completely replaced by the freshly deserialized data. This leaves the index and metadata maps in an inconsistent state — old vectors in the index with no corresponding metadata entries — which would cause key-not-found errors insearch().Add an explicit clear of the USearch index before loading:
Prompt To Fix With AI
sdk/runanywhere-commons/src/features/rag/onnx_embedding_provider.cpp, line 748-789 (link)embed_batchacquires lock only for batches > 1, creating inconsistent locking contractWhen
texts.size() == 1,embed_batchreturns early by delegating toembed(), which internally acquiresembed_mutex_. For all larger batches,embed_batchitself acquiresembed_mutex_(line 763) before callingembed_sub_batch(which does not lock). This creates an inconsistent locking pattern:size == 1are serialized byembed_mutex_insideembed()✓size > 1are serialized byembed_mutex_insideembed_batch()✓embed()call andembed_batch(size > 1)call are correctly serialized ✓While this happens to be race-free, the dual-path locking is fragile: the pre-allocated buffers (
input_ids_buf_,attention_mask_buf_,token_type_ids_buf_) are shared betweenembed()andembed_sub_batch(). Ifembed_sub_batchis ever called withoutembed_mutex_held (e.g., from a new code path), it would race. Consider moving the lock acquisition toembed_sub_batchitself, or documenting clearly thatembed_sub_batchmust always be called underembed_mutex_.Prompt To Fix With AI
Last reviewed commit: 67b4c22